Skip to main content
Every HTTP request to the MDCMS server passes through a layered middleware chain before reaching a route handler. This page documents the full request flow, authentication mechanisms, RBAC model, API key scopes, and response envelope format.

Middleware Chain

The server processes each request through the following stages, in order.
StageResponsibility
1. CORSValidates the Origin header against MDCMS_STUDIO_ALLOWED_ORIGINS. Handles OPTIONS preflight requests for Studio browser routes. Rejects disallowed origins with 403 FORBIDDEN_ORIGIN.
2. Request LoggingCaptures request metadata (method, URL, timing) for observability.
3. CSRFFor cookie-authenticated mutations, verifies the X-MDCMS-CSRF-Token header matches the mdcms_csrf cookie. API key requests bypass CSRF since they use Authorization headers.
4. AuthenticationIdentifies the calling principal — either an API key (via Authorization: Bearer mdcms_key_...) or a session (via cookie).
5. Target RoutingValidates that scoped routes include the required X-MDCMS-Project and/or X-MDCMS-Environment headers. Returns 400 if headers are missing.
6. AuthorizationChecks the authenticated principal’s permissions against the requirements of the target route (scope, role, resource path).
7. Route HandlerExecutes the business logic (content CRUD, schema sync, media upload, etc.).
8. Response EnvelopeWraps the result in a standard JSON response format with appropriate status codes and CORS headers.

Authentication Flows

MDCMS supports three authentication mechanisms, each designed for a different client context.

API Key Authentication

API keys are intended for server-to-server communication, CI/CD pipelines, and SDK usage.
Authorization: Bearer mdcms_key_abc123def456...
  • Keys use a mdcms_key_ prefix for easy identification in logs.
  • The key value is hashed (SHA-256) before storage — the raw key is only shown once at creation time.
  • Each key carries an array of operation scopes (e.g., content:read, schema:write) and a context allowlist restricting which project/environment pairs it can access.
  • Keys support optional expiration via expiresAt and soft revocation via revokedAt.

Session Authentication

Sessions are used by the Studio UI and browser-based interactions.
  • Created via better-auth after successful password, OIDC, or SAML authentication.
  • Stored in the sessions table with a session token cookie.
  • Inactivity timeout: 2 hours of no activity expires the session.
  • Absolute max age: 12 hours regardless of activity.
  • CSRF protection: Mutations require a X-MDCMS-CSRF-Token header matching the mdcms_csrf cookie (24-byte random token).
  • Supported identity providers: password (credential), OIDC (Okta, Azure AD, Google Workspace, Auth0), SAML.

CLI Device Flow

The CLI authenticates via a device-authorization-like flow:
1

Challenge creation

The CLI creates a login challenge with a 10-minute TTL, specifying the target project, environment, redirect URI, and requested scopes.
2

Browser authorization

The user opens a browser URL, authenticates via session (password/OIDC/SAML), and authorizes the CLI challenge. The challenge status transitions from pending to authorized.
3

Code exchange

The CLI polls for the authorization code, exchanges it for a scoped API key, and stores the credentials locally. The challenge status transitions from authorized to exchanged.
Default CLI login scopes: content:read, content:read:draft, content:write, content:delete, schema:read, schema:write.

RBAC Model

MDCMS implements role-based access control with hierarchical scoping.

Roles

Roles form an ordered hierarchy. Higher roles inherit all capabilities of lower roles.
RoleLevelScope Restriction
viewer0Global, project, or folder prefix
editor1Global, project, or folder prefix
admin2Global only
owner3Global only
admin and owner roles are restricted to global scope by a database check constraint. They cannot be scoped to a specific project or folder prefix.

Scopes

Each RBAC grant is bound to a scope that determines where the role applies:
Scope KindFields RequiredMeaning
globalNoneApplies to all projects and environments.
projectprojectApplies to all environments within a specific project.
folder_prefixproject, environment, pathPrefixApplies only to documents whose path starts with the given prefix within a specific project and environment.

Role Capability Matrix

Capabilityviewereditoradminowner
content:readYesYesYesYes
content:read:draftYesYesYes
content:writeYesYesYes
content:publishYesYesYes
content:unpublishYesYesYes
content:deleteYesYesYes
schema:readYesYesYesYes
schema:writeYesYes
projects:readYesYesYesYes
projects:writeYesYes
user:manageYesYes
settings:manageYesYes

API Key Scopes

API keys use a fine-grained scope model independent of RBAC roles. Each key declares which operations it is permitted to perform.
ScopeDescription
content:readRead published content documents.
content:read:draftRead draft (unpublished) content documents.
content:writeCreate and update content documents.
content:write:draftLegacy scope for draft writes (aliases content:write).
content:publishPublish documents (create immutable version snapshots).
content:deleteSoft-delete and restore content documents.
schema:readRead schema registry entries and sync state.
schema:writeSync schema changes to the server.
media:uploadUpload media files to S3 storage.
media:deleteDelete media files.
webhooks:readList and inspect webhook configurations.
webhooks:writeCreate, update, and delete webhook configurations.
environments:cloneClone content between environments.
environments:promotePromote content from one environment to another.
migrations:runExecute content migrations.
projects:readRead project metadata.
projects:writeUpdate project settings.
API keys are scoped to specific project/environment pairs via the contextAllowlist. A key with content:read scope but an allowlist of [{project: "docs", environment: "production"}] cannot read content from any other project or environment.

Response Envelopes

All API responses use one of three standard envelope formats.

Single Resource

{
  "data": {
    "documentId": "550e8400-e29b-41d4-a716-446655440000",
    "path": "content/blog/hello-world",
    "type": "BlogPost",
    "locale": "en-US",
    "format": "mdx",
    "body": "# Hello World\n\nWelcome to MDCMS.",
    "frontmatter": {
      "title": "Hello World",
      "slug": "hello-world"
    },
    "hasUnpublishedChanges": false,
    "publishedVersion": 3,
    "draftRevision": 7
  }
}

Paginated Collection

{
  "data": [
    { "documentId": "...", "path": "..." },
    { "documentId": "...", "path": "..." }
  ],
  "pagination": {
    "total": 142,
    "limit": 25,
    "offset": 0,
    "hasMore": true
  }
}

Error

{
  "status": "error",
  "code": "UNAUTHORIZED",
  "message": "Authentication required.",
  "details": {
    "path": "/api/v1/content"
  },
  "requestId": "req_abc123",
  "timestamp": "2026-04-13T10:30:00.000Z"
}
Error responses always include a machine-readable code, a human-readable message, and an ISO-8601 timestamp. The optional details object provides context-specific debugging information. The requestId is echoed from the X-Request-Id header when present.

CORS Configuration

Studio browser routes (content, schema, media, auth, search, webhooks, actions, environments, collaboration, and Studio bootstrap) are protected by origin validation. Configure allowed origins via the MDCMS_STUDIO_ALLOWED_ORIGINS environment variable:
MDCMS_STUDIO_ALLOWED_ORIGINS=http://localhost:4173,https://admin.example.com
The server validates that the Origin header matches either the request’s own origin (same-origin) or one of the configured allowed origins. Requests from disallowed origins receive a 403 FORBIDDEN_ORIGIN response. Preflight OPTIONS requests are handled automatically with the appropriate access-control-allow-* headers.
In local development, the default docker-compose configuration sets MDCMS_STUDIO_ALLOWED_ORIGINS to the studio-example dev server origin. Add additional origins when embedding Studio in a separate application.