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.
| Record | Decided | Status |
|---|---|---|
| 0001 | Record architecture decisions | Accepted |
| 0002 | Plugin-based architecture | Accepted |
| 0003 | Tactical DDD inside plugins | Accepted |
| 0004 | Model-agnostic copilot provider | Accepted |
| 0005 | The copilot acts as its user, never as itself | Accepted |
| 0006 | The CMS is an MCP server, over one shared tool registry | Accepted |
| 0007 | One tool registry, two surfaces | Accepted |
| 0008 | GraphQL is a protocol adapter over the public content API | Accepted |
| 0009 | The copilot asks in the moment, then applies directly | Accepted |
| 0010 | Skills are prompt configuration, authored in two places and delivered in one | Accepted |
| 0011 | Rich text is a structured document, not an opaque string | Accepted |
| 0012 | One storage provider per deployment, passed as one object | Accepted |
| 0013 | SSO is a provider port, and the core owns the handshake | Accepted |
| 0014 | Export and import as a separate plugin, one hop deep | Accepted |
| 0015 | Alarms flag content, and never block a write | Accepted |
| 0016 | Webhooks deliver from a queue, never from the outbox subscriber | Accepted |
| 0017 | Publication is protected per content type, and an approval belongs to a revision | Accepted |
| 0018 | One mail provider per deployment, and the message is built where the secret is | Accepted |
| 0019 | Shared workspaces are a read-only exception to workspace isolation | Proposed, amended 2026-09-29 — shipped in 0.7.0 |
| 0020 | The schema builder writes code, and only in development | Accepted |
| 0021 | A storage provider may publish public URLs, and the operator decides whether the API reports them | Accepted |
| 0022 | The entry editor is one form in schema order, and its properties open on demand | Proposed — 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:
| Record | Amended by | What changed |
|---|---|---|
| 0002 | 0003 | “The database plugin owns no schema” gained one exception, the outbox_events table |
| 0005 §5–6 | 0009 | Writes no longer queue as proposals for later acceptance |
| 0005 §3 | 0007 | The tool catalogue became the shared registry, not a copilot-private one |
| 0007 §4 | 0009 | The auto-apply gate on apply tools is gone; an apply tool is offered whenever the matching read tool is |
| 0008 | itself | The GraphiQL playground, first ruled out, ships at GET /api/v1/graphql/playground when API docs are enabled |
| 0012 | 0021 | MediaPluginConfig gained publicUrls / publicUrlTypes, and a provider may declare a publicUrls capability; the config still names no backend |
| 0017 | itself | Review requests name reviewers; notes, “request changes” and bypass reasons were removed |
| 0019 | itself | Explicit 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
| Location | What 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 package | How that package works, and why it is that way |
.agents/skills/ | Authoring conventions, as machine-readable skills |
.cursor/BUGBOT.md | Recurring 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.