[data-reveal]{opacity:1!important;transform:none!important}
Building

Distributable module

Package a DMS extension module other projects install.

Most work on the DMS is a project that loads it and registers its own pages — see Project setup. The rarer case is packaging a distributable extension module that other projects install (the way the official cms-* modules do). The pages, components, DataTypes, auth, and lifecycle are identical to a project (the Building and Frontend sections); a module differs only in how it's packaged and loaded:

  • a consumer installs it and adds it to their antelope.config.ts as a package/git module, rather than wiring it as the project's local module;
  • it may define its own AntelopeJS interface so other modules can extend it — though many distributable modules publish no interface at all.

A DMS module is an ordinary AntelopeJS module that depends on @antelopejs-private/cms, registers a module + pages when it loads, and ships a Nuxt layer for any custom frontend.

Directory layout

One layout that works — the maintained modules use variations of it; structure yours however works best for you:

<module-name>/
<module-name>/
  src/
    index.ts                        # entry — construct/start/stop/destroy + RegisterModule + AddNuxtLayer
    db/                             # schema / models / tables  (if it owns data)
    pages/                          # @RegisterPage / @RegisterPageExtension      → see Pages & components
    routes/                         # HTTP controllers feeding components         → see Pages & components
    services/  utils/  types/       # internal logic / constants
  nuxt-layer/                       # the module's frontend layer                 → see Frontend layer
    nuxt.config.ts
    app/{components,composables,types}/
    i18n/locales/*.json
  package.json
  tsconfig.json

The lifecycle entry (src/index.ts)

The entry file has two jobs. Its side-effect imports run your decorators — @RegisterPage, @RegisterPageExtension, @RegisterDataType, the DataControllers — exactly as in a project. A module that only extends another module's pages (Extending another module's page) still needs those imports: the injection happens when the decorator runs, and applies whenever the target page registers, before or after. Its lifecycle exportsconstruct, start, stop, destroy, a framework concept (Creating a module) — place each registration at the right moment:

  • construct(config) receives the consumer's config and attaches the frontend layer (AddNuxtLayer); wiring an interface implementation (ImplementInterface) belongs here too.
  • start() begins runtime work: provision the DB schema (RegisterSchema), hydrate caches, schedule cron.
  • stop() / destroy() release those resources.

RegisterModule is the exception: it runs at import time, at the top level (details below).

src/index.ts
import "./db"; // side-effect imports — decorators register on import
import "./pages";
import { RegisterSchema } from "@antelopejs/interface-database-decorators";
import { AddNuxtLayer, RegisterModule } from "@antelopejs-private/cms/interfaces/cms/page";

export interface Config {
  /* whatever the consumer passes under this module's config */
}
let config: Config;

export const myModule = RegisterModule({
  id: "<name>", // pages attach with module: "<name>"
  title: "$your_module.title", // i18n key — see Localization
  description: "$your_module.description",
  icon: "i-ph-cube",
});

export async function construct(cfg: Config): Promise<void> {
  config = cfg;
  await AddNuxtLayer({
    /* the module's frontend layer — see AddNuxtLayer below */
  });
}

export async function start(): Promise<void> {
  await RegisterSchema("<module-name>"); // provision the schema once the DB is up
}

export function stop(): void {
  /* stop cron, deactivate subscriptions */
}

RegisterModule — the dashboard entry

RegisterModule(opts) registers a top-level module in the dashboard nav and returns a CategoryInfo. It runs at import time at the top level — typically in src/index.ts. It must fire before its pages register, so the module exists when they attach by setting module: "<id>". How pages attach, their URLs, and the access model are covered in Module navigation and access below.

src/index.ts
import { RegisterModule } from "@antelopejs-private/cms/interfaces/cms/page";

export const demoModule = RegisterModule({
  id: "demo", // pages attach with module: "demo"
  title: "Demo Module", // use a "$cms_demo.title" i18n key in real modules
  description: "A demo module to showcase the module system",
  icon: "i-ph-cube",
  // landingPage: "<page-id>",         // page shown when entering the module
  defaultCategory: {
    // groups the module's loose pages under a heading
    displayName: "Demo Pages",
    icon: "i-ph-files",
    order: 0,
  },
});

Module navigation and access

A module ships its own separate sidebarNavigation covers the dashboard tree it sits in. A page joins the module by setting module: "<id>", and its category decides where it lands inside: under a category descending from the module root (RegisterModule returns that root — nest yours under it), or under the module's defaultCategory — a generic "Pages" heading when none is declared. Three constraints are enforced at registration: the category must descend from the module root, module pages cannot use the Settings category (a module declares its settings pages inside its own sidebar), and the module must already exist — a page naming an unknown module throws.

Module pages resolve at /modules/<moduleId>/<urlSlug>. Entering the module lands on its landingPage when that page exists, otherwise on the first page by order; an empty module resolves to /modules/<id>.

A hand-written link or router.push({ path }) to a module page must include the /modules/<moduleId> prefix, or the frontend catch-all 404s — even with a clean backend build. Prefer reading fullSlug from the page/site layout over hardcoding paths.

Access is owner-only by design: the Modules group — and every page and category inside it — is visible to the platform owner alone, the holder of the * permission (Auth & permissions). On a SaaS deployment that means you, the maker — not your customers, who are tenant owners (SaaS mode). Page and component permission ids still exist, prefixed modules.<moduleId>.…, but they are module-scoped: they never appear in the grantable Roles tree, permission checks deny them to any non-owner regardless of stored role grants, and per-page permissions have no effect inside the module — the owner sees everything. Design module pages as owner/admin areas.

On the frontend, useCurrentModule() returns the module that owns the current route (Page context).

Expose an interface (optional)

A module can define its own AntelopeJS interface so other modules extend it. Authoring, implementing, and wiring interfaces is framework territory: Interfaces and Exporting interfaces are the reference. On the packaging side, declare each interface as an explicit exports subpath of your package.json so consumers can import it.