Beta . Ortha CMS is in public beta. Every plan is free until it ends.
Features Ortha CMS on GitHub

What Ortha CMS is

A plugin host, a code-defined content model, and a Postgres database you own — what that means in practice.

Documents 0.13.1 Updated Edit this page Report a problem

On this page

Ortha CMS is a self-hosted headless CMS: a React admin, a NestJS API and a PostgreSQL database. It ships as a lockstep set of @orthacms/* npm packages, MIT-licensed.

Two decisions shape everything else.

Content types are code

A content type is a function call in your repository, not rows in the CMS’s database:

export const article = collection('article', {
    label: 'Articles',
    publishable: true,
    fields: {
        title: field.text({ required: true, maxLength: 200 }),
        body: field.richtext(),
        author: field.relation({ to: () => author, required: true })
    }
});

That one declaration:

  • becomes a real content_article table with a title column,
  • generates the editor’s form,
  • defines the REST and GraphQL shapes,
  • is reviewed, versioned and rolled back like any other code.

The trade: changing the model is a deployment. If editors must add fields without an engineer, this is the wrong tool. In development the admin’s Content Model page can write the file and its migration for you — the schema builder — but what it produces is still code to commit and deploy.

Capability lives in plugins

The host has no domain logic. It turns a list of plugins into an app:

// apps/server/src/plugins.ts
export function buildPlugins(config: OrthaCmsConfig): ServerPlugin[] {
    return [
        DatabasePlugin({ connectionString: config.database.url }),
        IdentityPlugin(config.plugins.identity),
        WorkspacesPlugin(),
        ContentPlugin({ types: contentTypes }),
        MediaServerPlugin({
            provider: createLocalStorageProvider(config.plugins.media.storage),
            config: config.plugins.media
        })
        // …
    ];
}

Auth, content, media, localization, the audit log, mail, webhooks, the AI copilot and MCP are all plugins. Adding a capability means writing a plugin and adding a line — never editing the host.

What a client sees

curl 'https://cms.example.com/api/v1/content/article?pageSize=10' \
  -H 'Authorization: Bearer orthacms_…' \
  -H 'X-Workspace-Id: <workspace-id>' \
  -H 'X-Reader-Tags: premium'     # optional: which segments this reader belongs to

The same data is available over GraphQL (POST /api/v1/graphql) and MCP (POST /api/v1/mcp) with the same token. See the delivery API.

What comes in the box

CapabilityWhat you get
Content modellingCollections and singles, twelve field types, four relation cardinalities, generated Postgres tables, collapsible form sections and a two-column form layout
EditingA generated form per type, a structured rich-text editor, a media library, a ⌘K search palette
PublishingDraft and published states, required-to-publish validation, soft delete with restore, optional approvals per type
Version historyEvery save snapshotted, field-level diffs, restore and publish-from-version
LocalizationOne row per locale, shared and per-locale fields, translation groups, coverage reporting
DeliveryREST, optionally GraphQL, over the same data, guards and scopes
Access controlThree global roles (Admin, Contributor, Viewer), permission keys per route, workspaces, API tokens, per-type publication protection
Shared workspacesA workspace can share its published records read-only; others link to them through explicit per-type grants
Reader accessSegments restrict which published entries a reader sees — sent as X-Reader-Tags — on REST, GraphQL and MCP
Content checksAlarms flag entries at Error, Warning or Info without blocking a save or publish
Import and exportRecords with relations, files and locales, as JSON, JSON Lines, ZIP or CSV
AuditingAn append-only activity log written in the same transaction as the change, plus an Insights dashboard
IntegrationSigned, retried webhooks with a delivery log; invitation and reset mail over SMTP
AIOrtha CMS AI in the admin, behind one model port with two adapters, and an MCP endpoint for external agents

What is not in the box

No scheduled publishing

Something has to publish an entry. There is no scheduler and no worker process — the webhook sender, mail sender and alarms sweep are in-process intervals, not a job queue.

Also absent:

  • Full-text search beyond ILIKE over a type’s text columns; no vector search.
  • Real-time collaboration or websockets.
  • Per-workspace roles — a role is global; membership only decides which workspaces a user reaches.
  • Webhooks for media, account or workspace events — webhooks cover the entry lifecycle and a test ping, at-least-once and unordered.

The shape of a running system

  • apps/server — the NestJS API: /api for the admin, /api/v1 for the public content API, GraphQL and MCP, /reference for OpenAPI.
  • apps/admin — a static React bundle, served by the API process in a generated app (orthacms start).
  • PostgreSQL 16 — one database. Each plugin owns its tables and migrations.

No external queue, cache or search cluster. Outside services are only what you choose: object storage for media, an SMTP relay, a model backend.

Where to go next