Skip to main content
Build and run it yourself · Build on it
Build on it

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

  • id is kebab-case, 3 to 49 characters, and must not collide with a core module.
  • icon is a name from the allowlist in scripts/build-extension-modules.mjs, never a React component.
  • Each routes entry must have a real page.tsx.
  • Each aiToolNames entry must exist in the tool registry and belong to exactly one module.
  • docsUrl is a /docs/... path or an https:// URL. Core and optional first-party modules must resolve to a live guide.
  • settings cannot 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.