# 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
```
