Skip to content

The module manifest

module.json sits at the root of your bundle. The loader reads it synchronously, before anything else about your module runs.

Unknown top-level keys are rejected, not ignored. A misspelled key is a loud failure rather than a silently-inert setting — which is the right trade when the alternative is a module that boots and mysteriously does half its job.

{
"id": "uo",
"name": "Ultima Online",
"version": "1.0.0",
"coreApi": "^1.0.0",
"server": "server/index.js",
"client": { "entry": "client/dist/entry.js" },
"schema": "server/db/schema.sql",
"purge": "server/db/purge.sql",
"mounts": {
"public": ["/shard", "/atlas"],
"admin": ["/shard", "/uo-link"],
"player": ["/shard"]
},
"extensions": ["admin.users.detail"],
"capabilities": ["shard", "atlas", "market"]
}
Key Required Meaning
id yes ^[a-z][a-z0-9-]{1,31}$. The directory name, the installed_modules key, the URL segment, and the client registry key — all at once. Must equal the directory it was read from.
name yes Human label for the admin Modules screen
version yes Semver. Recorded on install; shown on failure
coreApi yes Semver range, checked against core’s MODULE_API_VERSION
server no Server entry point, relative to the module root. Absent means a client-only module
client.entry no The prebuilt ESM chunk, in a subdirectory — the directory it sits in is what gets served. Absent means a server-only module; present-but-empty is rejected, because it claims a client half and delivers none
schema no Idempotent SQL fragment, replayed every boot
purge no Destructive teardown. Required if schema is present
mounts no Declared route prefixes per tier
extensions no Core extension slots this module mounts into
capabilities no Opaque strings published to clients for feature detection

The loader compares your declaration against what your module actually registers, and rejects a mismatch in either direction. Declaring a prefix you never mount fails; mounting one you never declared fails too.

Prefixes are validated against ^/[a-z0-9][a-z0-9-]*$, and the keys must match what you register exactly.

Opaque strings, published by GET /api/v1/public/modules — and only while the module is started. Clients like the SPA and the Android app read them to decide what to show.

They are not permissions and not mount prefixes. Keep them stable: something outside your repository is branching on them.

Core exports a single semver string, currently 1.6.0. Your range is checked at boot, before your code is required.

A minor bump adds members without removing any or changing a signature, so ^1.3.0 keeps resolving against 1.6.0 — which is exactly why module-uo still declares ^1.3.0 and runs fine.

Use a caret range against the oldest core you actually support and test against. Pinning exactly buys nothing and strands you on the next additive release.

schema runs on every boot, so it must be idempotent — CREATE TABLE IF NOT EXISTS, and additive migrations written so a replay is harmless. Table names must be namespaced or allowlisted; the loader checks.

purge is required whenever schema is present, because a module that can create tables must offer a way to remove them. It is only ever run by an explicit purge — never as part of an uninstall.

Remember where purge.sql lives: inside the directory an uninstall deletes. Write it to be run by someone who no longer remembers what your module created.

MODULE_API.md §2.1 is normative for the manifest, and §2.6 for the schema fragments.