# Installed modules This directory is bind-mounted into the container at `/app/modules` (see `docker-compose.yml`). It is where **installed modules** live — the game-specific routes, tables, screens and nav that are not part of core. Design of record: [`docs/website/MODULE_SYSTEM.md`](../../docs/website/MODULE_SYSTEM.md); the normative contract module authors build against is [`docs/website/MODULE_API.md`](../../docs/website/MODULE_API.md). **Core ships no module.** This directory is empty in a fresh checkout, and the site runs cleanly that way — an empty `modules/` is the normal state for bare core, not a misconfiguration. Everything below is ignored by git except this README, which exists so the directory itself is tracked: `docker-compose.yml` bind-mounts it, and Docker recreates a *missing* bind-mount source as a root-owned directory the container user cannot write. ## Layout One directory per module, named for its id, each holding a prebuilt bundle: ``` modules/ uo/ module.json # the manifest the loader reads server/index.js # registers routes, streams, hooks server/db/schema.sql # tables, replayed every boot server/db/purge.sql # only ever run by an explicit purge client/dist/entry.js # prebuilt ESM chunk, served at /modules/uo/ ``` **Nothing here is compiled by the operator.** A module arrives already built — that is the whole point of the design. There is no install step that runs a bundler, and none that needs one. ## Installing a module Two supported paths, both writing the same `installed_modules` row: - **The admin panel** downloads the bundle from the module's release, verifies it against its `sha256`, and unpacks it here. - **By hand**, for a compose-managed host: unpack the bundle into a directory named for the module id, e.g. `tar -xf uo-1.0.0.tgz -C ./modules`. Either way, **adding or removing a module takes a restart.** The loader scans this directory synchronously at startup (`MODULE_API.md` §4.1); nothing placed here is picked up by a running server. ``` docker compose restart app ``` On boot each module is validated, mounted, its schema fragment replayed and its `onBoot` hook run — reaching `started`, or `startup_failed` with the stage and reason recorded. A module that fails to start does not stop the site: core, and every other module, carry on without it. ## Uninstalling Removing a directory and restarting is enough to stop a module serving. Note that this is *not* the same as an uninstall through the admin panel, which also marks the row `disabled` — a directory that simply vanishes leaves a row claiming to be enabled, which the loader records as `startup_failed`. A module's **tables and data are retained** in both cases. Dropping them is a separate, explicit, destructive purge; it is never bundled into an uninstall. ## Ownership The container runs as uid 1000 (`node`) and the admin panel writes here, so the app must be able to write this directory. It is created by your checkout, with your ownership. If they differ: ``` chown -R 1000:1000 modules ```