Add a module
Register a JSON manifest and the pages it names; nothing under extensions/ ever gets executed.
Updated September 19, 2026 3 min read
When a business needs another area of functionality, register a module so the workspace can discover it and control its availability. Add a manifest under extensions/ and implement the routes it declares, keeping the existing core module definitions intact.
Manifest
Create extensions/<your-module>.module.json:
{
"$schema": "./module-manifest.schema.json",
"id": "acme-inventory",
"name": "Inventory",
"description": "Stock levels, reorder points, and supplier lead times.",
"category": "delivery",
"defaultEnabled": true,
"navLinks": [
{
"id": "acme-inventory",
"label": "Inventory",
"href": "/admin/acme-inventory",
"icon": "Library",
"description": "Stock levels and reorder points",
"moreGroup": "Delivery"
}
],
"routes": ["/admin/acme-inventory"],
"aiToolNames": [],
"docsUrl": "/docs/delivery"
}
Then create src/app/admin/acme-inventory/page.tsx, run npm run build:extensions, and commit both the manifest and src/lib/revenue-os/extension-modules.generated.ts.
Contribute existing App records to Today
Collections and Radar declare a today object in their existing manifests, with
a supported source and a native href. The builder checks that the route belongs
to that module. Today includes only enabled contributions, reads bounded records
through their existing domain services and isolates source failures.
The current source adapters are collection_case and radar_opportunity.
Adding another source requires a reviewed adapter in today-snapshot.ts,
matching schema validation, tests and a shared demo fixture. A manifest cannot
execute arbitrary code, fetch providers or define a new lifecycle by itself.
example-inventory.module.json in that directory is a complete working example. It ships disabled.
Rules that fail the build
idis kebab-case, 3 to 49 characters, and must not collide with a core module.iconis a name from the allowlist inscripts/build-extension-modules.mjs, never a React component.- Each
routesentry must have a realpage.tsx. - Each
aiToolNamesentry must exist in the tool registry and belong to exactly one module. docsUrlis a/docs/...path or anhttps://URL. Core and optional first-party modules must resolve to a live guide.settingscannot look like secrets. Credentials go through an adapter.
What a manifest cannot do
It cannot execute code, override a core module, ship an icon component, or add a schema change. Schema changes are ordered files in migrations/. Nothing in extensions/ is executed. That is why an operator can read exactly what a module can reach before enabling it.
For module-owned API routes, use requireAdminForModule("your-module-id") in every handler and register the directories in MODULE_API_DIRECTORIES (scripts/verify-module-route-guards.mjs) so the verifier checks that boundary. Adding a directory to the verifier alone does not enforce runtime authorization.
New to the repository? Start with your first developer change before adding a write path.