Architecture Decision Records

July 30, 2026 ยท View on GitHub

An ADR captures one durable decision: the forces that made it necessary, what we decided, and what that costs. ADRs are the authoritative record of why the system is shaped the way it is. Specs explore options; an ADR records the one outcome we committed to.

This is the layer agents and humans should trust for decisions. If a spec in specs/, a row in docs/spec-history.md, or an old comment disagrees with an accepted ADR, the ADR wins.

Rules

  • One decision per record. If you are documenting several decisions, write several ADRs.
  • Immutable once accepted. Do not edit a decision out of an accepted ADR. To change direction, write a new ADR, set its Supersedes to the old one, and set the old one's Superseded by to the new one. The chain is the history.
  • Amend without erasing. When a later ADR changes only one bounded part of an accepted decision, set Amends on the new ADR and Amended by on the old one. State the withdrawn scope in both links. Use Supersedes when the old decision should no longer govern at all.
  • Concise and outcome-focused. An ADR is not a spec. State the decision so a reader can act on it without reading the exploration. Link the spec for the deep evidence if it still exists; otherwise cite the git ref.
  • Status is one of: Proposed, Accepted, Superseded.
  • Decisions are born from specs but do not live there. When a design pass settles something durable, harvest it into an ADR and let the spec be deleted.
  • Proposed is a transient state. Record a decision as Proposed when it crystallizes during design; flip it to Accepted when the work lands. A Proposed ADR that no in-tree spec references means its spec was deleted (the work landed): flip it, or supersede it if abandoned. bun scripts/check-doc-hygiene.ts flags orphaned and stale Proposed ADRs.

Numbering

NNNN-kebab-decision-as-sentence.md, zero-padded, monotonically increasing. The title is the decision stated as a declarative sentence, so the filename alone reads as the conclusion.

The number is allocated at merge time, not author time. It is owned by the merge, not the branch. Two branches that each grab "the next number" while branched will collide (this is why parallel work has produced duplicate numbers before). So a branch may carry a provisional number; the person merging reconciles it to the true next-free integer, accounting for other open ADR PRs, before it lands. Whoever merges first keeps the number; a later branch that picked the same one renumbers. The ADR-NNNN citation form stays stable once merged; only the pre-merge placeholder is negotiable.

Template

# NNNN. <decision stated as one declarative sentence>

- **Status:** Proposed | Accepted | Superseded
- **Date:** YYYY-MM-DD
- **Supersedes:** [ADR-MMMM](MMMM-*.md) (or omit)
- **Superseded by:** [ADR-PPPP](PPPP-*.md) (added only when this is retired)
- **Amends:** [ADR-MMMM](MMMM-*.md) with the bounded change (or omit)
- **Amended by:** [ADR-PPPP](PPPP-*.md) with the bounded change (or omit)

## Context

The forces in play: what was true, what pressure forced a decision. No survey of
alternatives yet, just why a decision was needed. Two to five sentences.

## Decision

The single thing we decided, in active voice, present tense. A reader should be
able to act on this paragraph alone.

## Consequences

What becomes true, easier, harder, or deleted as a result. Name the trade-off
honestly, including what this forecloses.

## Considered alternatives (optional)

Each option and the one reason it lost. Terse. This is not the spec.

Index

ADRDecisionStatus
0001One classified scan(), no valid-only default readSuperseded by 0175
0002Stored entries reconcile to four visible read statesSuperseded by 0003
0003Stored entries reconcile to three visible read statesAccepted
0004Trust the relay; reject zero-knowledgeAccepted
0005Child docs are bound through the workspace, not the componentSuperseded by 0126
0006Schema evolution keeps the version tuple and refuses repair APIsSuperseded by 0125
0007Local shortcuts sync, global shortcuts stay per-deviceAccepted
0008rdev backs the desktop global triggerSuperseded by 0117
0009The CLI dispatches through a mandatory daemon; automation lives in library scriptsAccepted
0010Whispering exports recordings as a zip; continuous Markdown is the mount's jobAccepted
0011Rust owns the macOS dictation capability; the frontend is a view over itAccepted
0012Transcription settings are read at use; Rust's model cache owns mechanism, not configAccepted (model ownership amended by 0180)
0013File import is a surface, not a recording modeAccepted
0014View transitions morph a re-expressed glyph, not its containerAccepted
0016Prewarm the cold model load and refuse the rest of the latency menuAccepted
0017Pause system media playback while recording through one cross-platform controllerAccepted (VAD timing revised by 0027; default reaffirmed by 0045)
0018macOS resume is gated on a CoreAudio output read, not a MediaRemote read shimAccepted (no-op consequence corrected by 0045)
0019Global shortcuts have a permission-free floor; Accessibility is an opt-in tierSuperseded by 0117
0020macOS drives its keyboard tap with an owned CGEventTap, not the rdev forkAccepted
0021Actions are the only surface that crosses a process boundaryAccepted
0022Rust owns the models folder, the webview owns the catalogAccepted
0023Whispering separates its identity mark from Lucide controlsAccepted
0024An always-on worker runs app semantics beside the app-blind anchorSuperseded by 0043
0025Agent conversations are durable child docs driven by an observing workerProposed
0026The Matter vault's SQLite mirror is a read-only projection, never a verdict sourceAccepted
0027Playback pause tracks the speaking window; VAD pauses per utteranceAccepted
0028Both shortcut tiers share one physical KeyBinding modelAccepted
0029A matter.json marks a table; matter is a declared store, not a discovered lensAccepted
0030Agents are immutable capability bundles; arbitrary code runs only on a trusted boxAccepted
0031Collaboration is addressed single-writer regions in a child docAccepted (supersedes 0025)
0032A folder is a table or a container of tables, never both (owns reference resolution)Accepted
0033A conversation is a synced doc answered only by in-process peers; the cloud is a metered inference streamAccepted
0034The cloud doc-generation queue is withdrawnSuperseded by 0033
0035Durable storage is one per-person coordination box: an app-blind anchor and storeAccepted
0036An answer body is a native parts array; its text streams into Y.TextSuperseded by 0047
0037Adapter construction is a shared leaf package keyed on the model catalogSuperseded by 0050
0038A daemon answers through the first inference backend it can satisfy: byok, else opted-in metered, else host without answeringSuperseded by 0049
0039Dictation feedback is a projection of one lifecycle state, not an event logAccepted
0040A cursor write that cannot paste falls back to the clipboard, decided from the grantAccepted
0041Every answerer is a worker; the browser never answersSuperseded by 0043
0042The agent loop is the worker's, over the doc as the message arraySuperseded by 0047
0043An agent answers where its capability lives (supersedes 0041 every-answerer)Superseded by 0047
0044Tool approval is a per-conversation policy, resolved per call (auto / ask / deny)Accepted (design; approval mechanism revised by 0047)
0045Playback pause ships opt-in because macOS resume can start unrelated mediaAccepted
0046A capability-free agent persists finished messages, not live doc streams (scopes 0036)Superseded by 0047
0047The agent loop runs in the client; tools are dispatched actions, and the daemon provides data, not inferenceAccepted (design; core deleted as consumers migrate)
0048A conversation's loop is chosen by whether its transcript syncs across peersSuperseded by 0051
0049Inference is its own box; the daemon never infers; the client loop talks to a swappable inference serverAccepted
0050The inference contract is OpenAI-compatible Chat Completions; Epicenter's backend is one swappable gatewayAccepted
0051There is one agent loop; its store seam chooses persistence, so tab-manager needs no second loopAccepted (supersedes 0048)
0052Shortcut reach is the minimum of command, key, and platform ceilings, never a user toggleProposed
0053The Epicenter bearer is an audience-scoped credential; auth.fetch attaches it only to its originAccepted
0054An inference backend is either the metered Epicenter gateway or a custom OpenAI-compatible serverAccepted
0055Conversation storage is one canonical table in @epicenter/chat; every chat surface syncsAccepted
0056Local inference is a delegated engine behind the OpenAI-compatible seam; the runtime is a swappable defaultAccepted
0057Assistant markdown renders as a shared component tree, not a sanitized HTML stringAccepted
0058Push-to-talk owns the recording it starts, keyed by the recording's id, not a general lifecycle layerAccepted
0059An inference connection is a capability-orthogonal device endpoint; the model is per-conversation (amends 0054)Accepted (auth axis amended by 0060)
0060An inference connection is a base URL and an optional bearer key; everything non-static is an injected transport (amends 0059)Accepted
0061Local Books serves row-level facts from the mirror, computed reports live, and writes back through one approved verbAccepted
0062Local Books stores OAuth tokens in a single 0600 fileAccepted
0063The Local Books mirror is a multi-writer cache made safe by one monotonic write door, not single-writer disciplineAccepted
0064The Local Books mirror keeps one realm CDC cursor; table existence is the per-entity init latchAccepted
0065Matter is a standalone disk-as-truth tool; its SQLite mirror is a first-class read-only query surface under epicenter matterAccepted
0066Runtime portability is per-concern injection, not a runtime objectAccepted
0067Auth owns the /api/session endpoint; the data client is owner-scoped and receives ownerId at constructionAccepted (amended by 0092)
0068Privacy is a deployment, not a product feature; the hosted app carries zero privacy-configuration surfaceAccepted
0069Epicenter is one runnable program (the star) plus a la carte services addressed by base URL and tokenAccepted
0070Self-host adds no new ownership or auth mode: single-user is a preset, and only the credential source variesSuperseded by 0075
0071OAuth is hosted-only; a custom instance requires a tokenAccepted
0072Local Books ships as a standalone CLI; the ADR-0047 daemon surface is deferred behind a verb-core seamAccepted
0073Tools speak MCP natively; Epicenter owns only the transport MCP lacksAccepted (historical relay-floor channel layer deleted by ADR-0079/0086)
0074The secret vault is an owner-scoped synced store encrypted under a server-derived keyring, not a passphrase vaultAccepted
0075Self-host is a single-partition instance behind one operator-supplied bearer; multi-tenancy is Cloud-onlyAccepted (amended by 0092)
0076The relational-auth substrate (Better Auth + Postgres) is a Cloud-only layer; the instance composes neitherAccepted
0077Parsed-row memoization belongs to the table; the Svelte adapter is a stateless viewAccepted
0078Inference is a URL-addressed connection reached through a direct endpoint or gatewayAccepted (relay-floor tool carrier superseded by ADR-0079/0086)
0079Cross-device is two planes: Epicenter syncs the CRDT, the box is reached directly as a URL-addressed connectionAccepted (capability-layer deletion landed 2026-07-02)
0080The super app is a desktop host; cross-device is remote access to the session, not a per-app capability planeAccepted (the desktop-host decision is settled; a hosted session broker for turnkey mobile remote is the open product question)
0081Per-upstream OAuth concurrency decides whether a materialized mirror is box-owned (Local Books) or device-local (Gmail), not a property of "cloud-upstream apps" as a categoryAccepted (per-device reach narrowed to desktops by 0116)
0082Local Mail syncs by push-free history.list pollingAccepted (the OAuth-client-identity half moved to 0188)
0083apps/email is refused; Local Mail is the only Gmail clientAccepted
0084Super Chat's shell is a Bun-hosted local server, not a bundled SPAAccepted
0085A box is a role an addressable endpoint plays, not a distinct node typeAccepted
0086There is no live consumer for network-reachable capability reach; opensidian's cross-device tools are superseded by the super app, not migratedAccepted
0087Honeycrisp is the maintained notes product from one isomorphic workspace surfaceAccepted (one package contract amended by 0130)
0088Sign-in is an enhancement, never a doorAccepted
0089The blob store is a presigned-S3 kernel and the bucket is its only indexAccepted (key prefix amended by 0092)
0090The blob layer stays plaintext; confidentiality belongs to the encrypting consumerAccepted
0091Blobs trade a file for a durable content-addressed URL; documents are the only manifestAccepted (URL shape amended by 0092)
0092Identity is the partitionAccepted
0093KV metadata belongs to the workspace kv namespaceSuperseded by 0130
0094The connection is the boot decision: one connect callAccepted
0095WebSocket room auth uses route-owned subprotocol bearersAccepted
0096Local workspace persistence is environment-injectedAccepted
0097Super Chat tool modules receive a host APISuperseded by 0111
0098Every Local Mail concept a human acts on round-trips through Gmail API stateAccepted
0099Replace Transformations with a Dictionary, an always-on Polish, and a portable Recipe libraryAccepted
0100AI credits are product-priced units; the charge shape follows whether cost is known before the callAccepted (STT overspend remedy refined by 0103)
0101Native typed-row projection views are earned serially; board is firstAccepted
0102Vocab stores verbatim entries under a human-owned note and refuses glosses, SRS, and provenanceAccepted
0103STT overspend is bounded by cheap guards in trigger order, never by media preflight or a reservation lockAccepted
0104Hosted models are a build-time seed catalog, not a discovered one; a runtime overlay is a named deferralAccepted
0105Vocab is a multilingual tutor and readings are a client-side derived viewAccepted
0106A child-doc body owns one layout; the polymorphic timeline is refused until a product earns itAccepted (row-body layout amended by 0135)
0107A child-doc text body is a plain Y.Text; the timeline array is deletedAccepted (row-body root amended by 0135)
0108Third-party provider credentials are selected by the app's target provider-environment, encoded in the secret name and resolved by one injected helperAccepted
0109Hosted Tauri auth keeps app-owned keyring edges until three real callers earn sharingAccepted
0110Edit write timing follows the value owner; there is no debounce tierAccepted
0111Super Chat v1 exposes built-in Epicenter apps and defers extension surfacesAccepted
0112The CLI watcher is not a callable action serverAccepted
0113Super Chat session commands are host-owned; transports only frame themAccepted
0114Hosted auth surfaces are plain tool logins, not marketing pagesAccepted
0115Super Chat remote attach rides an endpoint-addressed trusted AttachRelay; one consumer, not the deleted route-addressed relay floorAccepted
0116Local Mail is desktop-first: one Bun engine, no background mail service, the open app owns the sync loopAccepted
0117Global shortcut input is plugin chords only, and the macOS tap is just the paste grant watcherAccepted
0118Epicenter is one trusted Bun-hosted SPA originAccepted (app-window HTTP authority amended by 0179)
0119Complete record maps sync through schema-blind server-ordered patchesSuperseded by 0131
0120Fields validate present values and table lenses own presenceAccepted (read surface amended by 0175)
0121Background sync resolves key conflicts by server orderAccepted
0122Logical records are portable; SQLite files and views are runtime stateAccepted
0123Bounded metadata uses record authority; merge-sensitive state uses lazy child documentsSuperseded by 0124
0124Workspace documents are top-level parameterized resourcesSuperseded by 0130
0125Record definitions are release-local lenses and never migrate user dataAccepted
0126Child documents use format capabilities and evolve outside records databasesSuperseded by 0124 (touch policy amended by 0128)
0127Chat streams live turns in client state and stores finished messages as recordsSuperseded by 0152
0128Tables do not declare document-edit touch policy without a runtime ownerAccepted
0129Matter and Workspace share fields, not authority policyAccepted
0130Workspace definitions expose tables with row-owned documents and a release-local KV lensAccepted
0131Row sync folds sealed RowIntent rounds without refusalSuperseded by 0141
0132Workspace KV is one reserved immortal rowAccepted
0133The workspace authority stores documents as sequence-addressed update logsSuperseded by 0141
0134Replicas store confirmed state and compacted RowIntentsSuperseded by 0141
0135Row documents have application-owned rootsAccepted
0136Replica baseline acquisition uses a disposable anchored live scanSuperseded by 0142
0137Hosted storage is one eventually enforced physical account allowanceAccepted
0138Device and account workspace adoptionSuperseded by 0139
0139Account runtime open adds device state through native intentsSuperseded by 0143
0140Open workspaces synchronize automatically and callers settle one watermarkAccepted
0141Authority current state and receipt watermarks drive row convergenceAccepted
0142Bootstrap, history gaps, and lineage mismatches have distinct recoveryAccepted
0143Account open never consumes Device dataAccepted
0144Scalar rows and row documents synchronize through independent client planesSuperseded by 0171
0145One account authority owns every workspace and one socket per open row documentProposed
0146Row documents use one Yjs 14 major and runtime-native update logsProposed
0147Cross-plane transfer and recovery use logical coordination, not atomic snapshotsProposed
0148Blobs use opaque identifiers rather than content hashesSuperseded by 0173
0149Local blob stores are canonical and remote replication is explicitSuperseded by 0171
0150Whispering uploads operator-readable audioAccepted
0151Local workspace stores use owner-first directoriesAccepted
0152Epicenter Home is a shell above workspacesAccepted (installation boundary amended by 0179)
0153Trusted apps are source-built static catalog membersSuperseded by 0179
0154Blob access is address-onlyAccepted
0155Epicenter desktop auth is one Bun authority with credential-free windowsAccepted
0156Applications bring workspace lenses; runtimes own workspaces by IDAccepted
0157Read-only SQL exposes one schema-opaque row relationAccepted
0158Installed apps declare workspace IDs but run no Bun modulesSuperseded by 0160
0159Row documents persist in one owner-side SQLite update logAccepted
0160Lenses interpret durable namespaces without creating lifecycle scopesProposed
0161Each person has one Epicenter replicated on each adapter boundaryProposed
0162Epicenter Home owns relational inspection; applications receive no SQLProposed
0163Scalar sync separates fact reads from numbered intent submissionsProposed
0164Scalar facts converge independently; Epicenter refuses distributed transactionsProposed
0165Browser origins contain independent Epicenter replicasAccepted
0166Data, document sync, and agent replace WorkspaceProposed
0167A portable Epicenter is an identity-free export of one authority cutProposed
0168Lenses are complete pure JSON interpretationsProposed
0169Row references are non-enforcing table interpretationsProposed
0170One live Epicenter has sealed Backups; Restore creates a fresh authority lifetimeProposed
0171Every durable local write leaves an automatic authority obligationProposed
0172SQLite stores convergent facts and documents; raw files store blob bytesProposed
0173Each row owns at most one write-once immutable blobProposed
0174Row documents project as nullable compact cells and persist as bounded live chainsProposed
0175Table traversal is complete and classified, with paging kept privateAccepted
0176Lenses declare no query capabilities; indexed reads require separate ownersAccepted
0177A browser replica is owned by a storage-partition and origin pairAccepted
0178Row facts and value facts are separate relations keyed by structured coordinatesAccepted
0179An installed app is an inert built folder admitted through one static-artifact boundaryAccepted (HTTP authority restored by 0185; native commands amended by 0186)
0180Epicenter has one host-owned active local transcription modelAccepted
0181Every app receives one portable Epicenter capability handleAccepted
0183Epicenter mediates the effects it owns and names the rest unmediatedAccepted (ordinary HTTP observation withdrawn by 0185)
0184One host recorder progressively stages each claimable recording until its owner stops or cancels itAccepted
0185Trusted app HTTP uses Tauri's standard transport without observationAccepted (capability carriers clarified by 0187)
0186An app reaches Epicenter through one bundled MIT client it installs itselfAccepted (amends 0181 at delivery and namespace, 0179 at the native command surface; data settled by 0187)
0187A bound handle reports staleness; tables can name rows, values cannotAccepted
0188The Gmail application identity belongs to the shipped distribution, and no Epicenter server enters the Gmail pathProposed (direction locked; flips to Accepted when the official Google client is provisioned and verified)

When you add an ADR, add its row here.