docs(website): add the module system design of record #122
Reference in New Issue
Block a user
No description provided.
Delete Branch "docs/module-system"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds
website/MODULE_SYSTEM.md— the design of record for turning Runic Gateway from a UO-specific platform into a game-agnostic one. Game-specific routes, tables, screens and nav leave the core website and become an installable module. An operator installs the base site, installs the module for their game, and restarts. They never build anything — the WordPress-plugin model, and the constraint that drives most of the design.Docs only. No code changes, nothing implemented.
What this is
The draft plan was checked line by line against the working trees of
website/,link/,servuo-plugins/,installer/anddocs/. Part 1 of the document records what is actually there; Part 2 is the plan that survives it; Part 3 lists the ten settled decisions.What the draft got right
The extraction really is close to a folder move, and that is now verified rather than assumed:
router/v1/{public,admin,player}/index.jsare pure mount tables — module mounting drops straight inutils/files import only four things from core, which is where the API surface comes fromWhat did not survive verification
/api/v1/game/uo)registerModuleNavas a server APInavOverrides.js:13-21explicitly refuses to have one. Nav registration is client-sidefeature:gate mechanism is itself the shard visibility systemschema.sqlis replayed idempotently every boot. Modules ship fragmentsserver.jsholds eight UO call sites with no hook to receive them →onBoot/onShutdownnotificationStreams.js,pushDispatch.fromShardEventandannounceWorker's town-crier leg are entangled and need registriesGET /admin/users/:idserved by the UO controllerwebsite/bot/src. Nothing to extractinstaller/src/cli.rs:162— "The installer never contacts your website" — andBundleis hardcoded to two non-optional componentssha256inside a bundle JSON over anonymous HTTPS, no signaturesrouteManifest.jsandswagger.jsboth walkapp.jswith no DB — modules must mount synchronously from the filesystemThe client-loading decision
Three constraints had to hold at once: the operator builds nothing, production pulls a prebuilt image (
Dockerfilebakesclient/dist, compose has nobuild:), andconfig/csp.js:49setsscript-src 'self'with no'unsafe-inline'— which kills import maps, since those must be inline.What survives all three: the module's own CI builds its client half in Vite library mode with React externalised; core hands React over on a global (
window.__rg) rather than an import map, so there is exactly one React instance; andhtmlShell.js:111— which already rewrites</head>— injects a same-origin<script type="module" src>, which'self'allows with no nonce. Modules live on a mounted volume likeuploadsalready does.Phase 1 prototypes this end to end before anything is committed to it, since it is the highest-risk call in the plan.
Deliberate breaking change
SPA pages get namespaced —
/uo/*,/admin/uo/*,/player/uo/*— with no redirects and no nav-override migration, on the basis that the site is not public yet. API URLs are unaffected, so the Android app and Discord bot need no API changes.Two accepted consequences are recorded in §2.8: stored nav-override rows are keyed by path and must be redone, and
android-app/.../NavPaths.ktholds ten/site/*constants that will need updating in the separate Android plan. App Links verification is unaffected — its intent filters only cover/mobile/callback.Phasing
/api/v1/public/atlas/*behind the proposed surface, plus the chunk load under the enforced CSP/api/v1/public/modules, client registry. Zero URL changes or it is wrongmodule-uo— 25 tables, ~35 files, four acceptance criteria including a CI grep test for UO identifiers left in core🤖 Generated with Claude Code
https://claude.ai/code/session_018ocYxQWk3EhZe5gWRJXFU8
Turns Runic Gateway from a UO-specific platform into a game-agnostic one: game-specific routes, tables, screens and nav leave the core website and become an installable module. Operators install the base site, install the module for their game, and restart -- they never build anything. Verified against the working trees rather than written from the draft. The draft's load-bearing assumptions that did not survive: * one flat module route prefix cannot coexist with URL preservation, since UO routes span three access tiers -- modules own a named slot per tier * there is no server-side nav list, and navOverrides.js refuses to have one, so nav registration is client-side * the nav feature-gate mechanism is itself the shard visibility system * there is no migration system to model a module runner on -- schema.sql is replayed idempotently every boot, so modules ship fragments * boot/shutdown holds eight UO call sites with no hook to receive them * notificationStreams, pushDispatch and announceWorker are entangled, not moves * six UO routes are nested under the core users resource * the Discord bot has no UO logic at all * the installer never contacts the website and its Bundle is hardcoded to two components, so delivery is website-side * routeManifest and swagger walk app.js with no DB, so modules mount synchronously from the filesystem * production is a prebuilt pull-only image and CSP forbids inline script, which together decide how the client half loads Ten decisions are recorded as settled in Part 3. Co-Authored-By: Claude <noreply@anthropic.com>