Skip to main content
MDCMS is built on a module system that organizes all server-side functionality into discrete, composable packages. Modules extend three surfaces — the HTTP server, the CLI, and the Studio UI — through a single, validated contract.

Module Concept

A module is a self-contained unit that can contribute to one or more runtime surfaces:
SurfaceWhat it provides
ServerHTTP route handlers mounted on the Elysia app, plus action catalog entries for discoverable operations.
CLIAction aliases, output formatters, and preflight hooks that extend the mdcms command.
StudioUI extension points including routes, navigation items, slot widgets, field kinds, and editor nodes.
Modules declare a manifest with metadata (id, version, API version, dependencies) and one or more surface implementations.

Module Package Type

Every module conforms to the MdcmsModulePackage type exported from @mdcms/shared:
type MdcmsModulePackage<App = unknown, AppDeps = unknown> = {
  manifest: ModuleManifest;
  server?: ServerSurface<App, AppDeps>;
  cli?: CliSurface;
};

type ModuleManifest = {
  id: string;
  version: string;
  apiVersion: "1";
  kind?: "domain" | "core";
  dependsOn?: string[];
  minCoreVersion?: string;
  maxCoreVersion?: string;
};

type ServerSurface<App, AppDeps> = {
  mount: (app: App, deps: AppDeps) => void;
  actions?: ActionCatalogItem[];
};

type CliSurface = {
  actionAliases?: CliActionAlias[];
  outputFormatters?: CliOutputFormatter[];
  preflightHooks?: CliPreflightHook[];
};
The manifest.kind field distinguishes infrastructure modules (core) from business-domain modules (domain). The dependsOn array declares inter-module dependencies that the loader validates at boot time.

Loading Lifecycle

Modules are registered at compile time in packages/modules/src/index.ts and loaded during server startup through a three-phase process:
1

Build load report

buildServerModuleLoadReport() validates every registered module package against the Zod schema, checks manifest compatibility (API version, core version range), resolves the dependency graph, and produces a ServerModuleLoadReport.
2

Load modules

loadServerModules() reads from the installedModules compile-time registry and delegates to the build step. If any module fails validation or has unresolvable dependencies, the server refuses to start.
3

Mount modules

mountLoadedServerModules() iterates the loaded modules in dependency order and calls each module’s server.mount(), passing the Elysia app instance and shared dependencies (database DAL, auth service, etc.).
// apps/server/src/lib/module-loader.ts (simplified)
const report = loadServerModules({ coreVersion: env.APP_VERSION, logger });
const actions = collectServerModuleActions(report);
mountLoadedServerModules(app, deps, report);

Core Modules

MDCMS ships with two first-party modules:
Kind: coreProvides the foundational infrastructure surface: health checks (/healthz), Studio bootstrap endpoint, authentication routes, user management, API key management, RBAC grant management, and invite flows.
Kind: domainProvides the content domain surface: document CRUD, schema registry sync, media upload and management, webhook dispatch, environment cloning/promotion, search, and content migration execution.
Both modules implement server and cli surfaces. The module registry in packages/modules/src/index.ts sorts them deterministically by manifest id before exposing them to the loader.

Action Catalog

Every server-side operation is registered as an action — a discoverable, self-describing API endpoint. Actions are collected from all loaded modules and exposed through a unified catalog.
type ActionCatalogItem = {
  id: string; // e.g. "content.documents.list"
  kind: "command" | "query"; // write vs read
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
  path: string; // e.g. "/api/v1/content"
  permissions: string[]; // required scopes
  studio?: StudioActionMeta; // visibility, surface, label, form hints
  cli?: CliActionMeta; // visibility, alias, input mode
  requestSchema?: JsonSchema; // JSON Schema for request body
  responseSchema?: JsonSchema; // JSON Schema for response body
};
The catalog is queryable at runtime:
  • GET /api/v1/actions — Returns all actions visible to the current principal.
  • GET /api/v1/actions/:id — Returns a single action by id.
Actions support visibility policies that filter the catalog per-request based on the caller’s authentication context and permissions.
The CLI uses the action catalog to dynamically discover available commands. Run mdcms actions to see all actions available for your current credentials.

Studio Extensibility Surfaces

The Studio component exposes several extension points that modules can populate:
SurfaceDescription
routesAdditional React Router routes mounted inside the Studio shell.
navItemsNavigation entries added to the Studio sidebar.
slotWidgetsWidgets rendered in named layout slots across Studio pages.
fieldKindsCustom form field renderers for schema-driven frontmatter editing.
editorNodesCustom TipTap/ProseMirror nodes for the MDX editor.
actionOverridesReplace or wrap default action behaviors (e.g., custom publish flow).
settingsPanelsAdditional panels on the Studio settings page.
Standard slot IDs used across Studio pages:
Slot IDLocation
dashboard.mainMain content area of the dashboard page.
content.list.toolbarToolbar above the content list table.
content.editor.header.actionsAction buttons in the document editor header.
content.editor.sidebarSidebar panels in the document editor.
settings.generalGeneral settings section.
In v1, Studio extensibility surfaces are defined but only consumed by first-party modules. The contract is stable and documented for future third-party use.

v1 Limitation

The current module system supports first-party modules only. All modules are compiled into the server binary at build time via the packages/modules/ registry. There is no dynamic plugin loading, no remote module resolution, and no third-party module marketplace. The module contract (MdcmsModulePackage, ModuleManifest, compatibility checks) is designed with future extensibility in mind, but v1 restricts the surface to modules maintained within the MDCMS monorepo.

Multi-Tenancy Model

MDCMS implements a two-level isolation hierarchy: projects and environments.

Project Isolation

A project is the top-level tenant boundary. Each project owns:
  • Its own schema (content types and field definitions)
  • Its own content (documents and versions)
  • Its own environments (independent content spaces)
  • Its own media (uploaded files in S3)
  • Its own webhooks (event subscriptions)
  • Its own users and API keys (scoped access control)
Projects are identified by slug (e.g., marketing-site) and are completely isolated from one another at the database level.

Environment Isolation

Within a project, each environment maintains:
  • Independent documents — Editing content in staging does not affect production.
  • Independent versions — Publish history is per-environment.
  • Schema overlays — Environments that extend another inherit the base schema and can add or modify fields.

Target Routing

Every scoped API request must include explicit targeting headers:
X-MDCMS-Project: marketing-site
X-MDCMS-Environment: production
The server’s target routing guard validates these headers before the request reaches any route handler. Routes that require project+environment context (content, schema, media, webhooks, search) reject requests missing either header with a 400 error. Routes that require only project context (environments API) require X-MDCMS-Project alone.
API keys include a contextAllowlist that restricts which project/environment pairs they can access. A key scoped to production cannot be used to read staging content, even if it has content:read permission.