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

Architecture decisions

Twenty-two records covering why the system is shaped the way it is.

Documents 0.13.1 Updated Edit this page Report a problem

On this page

The Architecture Decision Records in docs/adr/ say why Ortha is built the way it is — including what was rejected and what it cost. Read the relevant one before proposing a structural change.

RecordDecidedStatus
0001Record architecture decisionsAccepted
0002Plugin-based architectureAccepted
0003Tactical DDD inside pluginsAccepted
0004Model-agnostic copilot providerAccepted
0005The copilot acts as its user, never as itselfAccepted
0006The CMS is an MCP server, over one shared tool registryAccepted
0007One tool registry, two surfacesAccepted
0008GraphQL is a protocol adapter over the public content APIAccepted
0009The copilot asks in the moment, then applies directlyAccepted
0010Skills are prompt configuration, authored in two places and delivered in oneAccepted
0011Rich text is a structured document, not an opaque stringAccepted
0012One storage provider per deployment, passed as one objectAccepted
0013SSO is a provider port, and the core owns the handshakeAccepted
0014Export and import as a separate plugin, one hop deepAccepted
0015Alarms flag content, and never block a writeAccepted
0016Webhooks deliver from a queue, never from the outbox subscriberAccepted
0017Publication is protected per content type, and an approval belongs to a revisionAccepted
0018One mail provider per deployment, and the message is built where the secret isAccepted
0019Shared workspaces are a read-only exception to workspace isolationProposed, amended 2026-09-29 — shipped in 0.7.0
0020The schema builder writes code, and only in developmentAccepted
0021A storage provider may publish public URLs, and the operator decides whether the API reports themAccepted
0022The entry editor is one form in schema order, and its properties open on demandProposed — shipped in 0.12.0

0001–0018, 0020 and 0021 are accepted. 0019 and 0022 are still marked Proposed, though what they describe ships — 0019 in 0.7.0, 0022 in 0.12.0. An accepted record is never rewritten: a change of mind is a new record, or an amendment noted at the top of the old one.

The ones that explain the most

0002 — plugin architecture. Why there is a host that owns nothing, and why capability lives in plugins. Everything about extending Ortha follows from this.

0005 and 0009 together — the copilot’s authority. 0005 established that a run has no identity of its own and holds exactly its user’s permissions. 0009 then amended it: the queue of proposals waiting to be accepted is gone, replaced by a prompt that appears before the call runs.

Reading 0009’s context section is the clearest statement of why a review step that produces twelve approvals for one sentence is worse than no ceremony at all. It is also the honest account of a feature being removed after shipping.

0008 — GraphQL as an adapter. Why there is no second implementation of the content API, and why the GraphQL schema is built per workspace grant set — so introspection cannot enumerate types the workspace was not granted.

0011 — rich text as a document. Why a body is a node tree rather than an HTML string, and what that makes checkable.

0015 — alarms never block. Why a severity has no authority, and why the moment it could refuse a publish there would be two competing authorities on whether an entry is valid.

0016 — webhooks deliver from a queue. The outbox dispatcher calls its subscribers inside its claim transaction, so a subscriber that made an HTTP request would hold a pool client for a stranger’s response time, and a throw would count against the outbox row’s attempts — eventually parking an event the activity log was also waiting for. So the subscriber only queues a delivery row per endpoint, and a worker POSTs with nothing open. Read it before writing any subscriber that reaches the network.

0017 — publication protection. Why an approval is bound to a revision rather than to an entry, so an edit leaves it behind with no dismissal logic; why there is no third entry status; and why there is no approve tool for the copilot or over MCP, now or later. It is also the record that had to answer 0009 — reading the two together is the clearest account of when pausing on a human is worth its ceremony and when it is not. Its September update records three changes from first use: a review request names the reviewers asked, there are no review notes and no “request changes” vote, and a bypass is confirmed rather than given a written reason.

0018 — one mail provider. Why an invitation or reset message is queued inside the transaction that minted its token — the raw token exists nowhere else, so no outbox subscriber could rebuild the message — and why the worker still sends with no transaction open, as 0016 requires. Its seventh decision — self-service password recovery only with a provider, answering the same for every address and rate-limited by address and by source — is its second phase, which shipped in 0.13.0. See identity.

0019 — shared workspaces. Why a library workspace exposes only its published records, read-only, and why the visibility rule lives in one query (SharedSourcesQuery) instead of being restated on every read path. Its amendment replaced the implicit rule with explicit per-source shared grants (“Tags · Travel Library”), so sharing is opt-in on both sides. See shared workspaces.

0020 — the schema builder writes code. Why the Content Model page edits the TypeScript source and produces an ordinary drizzle-kit migration instead of keeping a schema in the database the way a runtime CMS would; why editing is refused in production; and why the builder owns only files carrying // @orthacms-generated. It also records the rule that the content model itself had followed without a record — in production no route changes the schema — and that the copilot may at most propose a schema change, never apply one, a deliberate exception to 0009. See the schema builder.

0022 — the entry editor is one form. Why every field, relations and media included, renders in one Content form in schema order, and tabs are only views of the record (Content, Access, History); why the Properties panel opens over the form on demand instead of taking a docked column; and why status is a list content renders from what plugins report (ENTRY_STATUS_ITEM_SLOT) rather than chips plugins draw. It also records that History absorbs the record’s activity (ENTRY_TIMELINE_SLOT), and that a guard still never restyles Publish (0017). It is the record plugin authors need: it changed what ENTRY_TAB_SLOT is for. See slots.

Amendments

No record has been superseded. Several have been amended in place, and reading only the original text will mislead you:

RecordAmended byWhat changed
00020003“The database plugin owns no schema” gained one exception, the outbox_events table
0005 §5–60009Writes no longer queue as proposals for later acceptance
0005 §30007The tool catalogue became the shared registry, not a copilot-private one
0007 §40009The auto-apply gate on apply tools is gone; an apply tool is offered whenever the matching read tool is
0008itselfThe GraphiQL playground, first ruled out, ships at GET /api/v1/graphql/playground when API docs are enabled
00120021MediaPluginConfig gained publicUrls / publicUrlTypes, and a provider may declare a publicUrls capability; the config still names no backend
0017itselfReview requests name reviewers; notes, “request changes” and bypass reasons were removed
0019itselfExplicit per-source shared grants replace “any workspace granted the type”; the public list became the union of visible sources

Everything else in 0005 stands — and carries more weight than before, since the capability profile and the prompt are now the whole of the enforcement.

Where else the reasoning lives

LocationWhat it holds
docs/adr/The decisions themselves
docs/design/Design write-ups for cross-cutting features, most of them now shipped
docs/artifacts/A dossier per package: data model, HTTP API, screens, invariants
AGENTS.md per packageHow that package works, and why it is that way
.agents/skills/Authoring conventions, as machine-readable skills
.cursor/BUGBOT.mdRecurring bug patterns reviewers watch for

The per-package AGENTS.md files are the most detailed material in the project: the reasoning behind each decision, the failure it prevents, and bugs that shipped and were fixed. Start there when a page here leaves you wanting more.