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
Supersedesto the old one, and set the old one'sSuperseded byto 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
Amendson the new ADR andAmended byon the old one. State the withdrawn scope in both links. UseSupersedeswhen 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.
Proposedis a transient state. Record a decision asProposedwhen it crystallizes during design; flip it toAcceptedwhen the work lands. AProposedADR 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.tsflags orphaned and staleProposedADRs.
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
| ADR | Decision | Status |
|---|---|---|
| 0001 | One classified scan(), no valid-only default read | Superseded by 0175 |
| 0002 | Stored entries reconcile to four visible read states | Superseded by 0003 |
| 0003 | Stored entries reconcile to three visible read states | Accepted |
| 0004 | Trust the relay; reject zero-knowledge | Accepted |
| 0005 | Child docs are bound through the workspace, not the component | Superseded by 0126 |
| 0006 | Schema evolution keeps the version tuple and refuses repair APIs | Superseded by 0125 |
| 0007 | Local shortcuts sync, global shortcuts stay per-device | Accepted |
| 0008 | rdev backs the desktop global trigger | Superseded by 0117 |
| 0009 | The CLI dispatches through a mandatory daemon; automation lives in library scripts | Accepted |
| 0010 | Whispering exports recordings as a zip; continuous Markdown is the mount's job | Accepted |
| 0011 | Rust owns the macOS dictation capability; the frontend is a view over it | Accepted |
| 0012 | Transcription settings are read at use; Rust's model cache owns mechanism, not config | Accepted (model ownership amended by 0180) |
| 0013 | File import is a surface, not a recording mode | Accepted |
| 0014 | View transitions morph a re-expressed glyph, not its container | Accepted |
| 0016 | Prewarm the cold model load and refuse the rest of the latency menu | Accepted |
| 0017 | Pause system media playback while recording through one cross-platform controller | Accepted (VAD timing revised by 0027; default reaffirmed by 0045) |
| 0018 | macOS resume is gated on a CoreAudio output read, not a MediaRemote read shim | Accepted (no-op consequence corrected by 0045) |
| 0019 | Global shortcuts have a permission-free floor; Accessibility is an opt-in tier | Superseded by 0117 |
| 0020 | macOS drives its keyboard tap with an owned CGEventTap, not the rdev fork | Accepted |
| 0021 | Actions are the only surface that crosses a process boundary | Accepted |
| 0022 | Rust owns the models folder, the webview owns the catalog | Accepted |
| 0023 | Whispering separates its identity mark from Lucide controls | Accepted |
| 0024 | An always-on worker runs app semantics beside the app-blind anchor | Superseded by 0043 |
| 0025 | Agent conversations are durable child docs driven by an observing worker | Proposed |
| 0026 | The Matter vault's SQLite mirror is a read-only projection, never a verdict source | Accepted |
| 0027 | Playback pause tracks the speaking window; VAD pauses per utterance | Accepted |
| 0028 | Both shortcut tiers share one physical KeyBinding model | Accepted |
| 0029 | A matter.json marks a table; matter is a declared store, not a discovered lens | Accepted |
| 0030 | Agents are immutable capability bundles; arbitrary code runs only on a trusted box | Accepted |
| 0031 | Collaboration is addressed single-writer regions in a child doc | Accepted (supersedes 0025) |
| 0032 | A folder is a table or a container of tables, never both (owns reference resolution) | Accepted |
| 0033 | A conversation is a synced doc answered only by in-process peers; the cloud is a metered inference stream | Accepted |
| 0034 | The cloud doc-generation queue is withdrawn | Superseded by 0033 |
| 0035 | Durable storage is one per-person coordination box: an app-blind anchor and store | Accepted |
| 0036 | An answer body is a native parts array; its text streams into Y.Text | Superseded by 0047 |
| 0037 | Adapter construction is a shared leaf package keyed on the model catalog | Superseded by 0050 |
| 0038 | A daemon answers through the first inference backend it can satisfy: byok, else opted-in metered, else host without answering | Superseded by 0049 |
| 0039 | Dictation feedback is a projection of one lifecycle state, not an event log | Accepted |
| 0040 | A cursor write that cannot paste falls back to the clipboard, decided from the grant | Accepted |
| 0041 | Every answerer is a worker; the browser never answers | Superseded by 0043 |
| 0042 | The agent loop is the worker's, over the doc as the message array | Superseded by 0047 |
| 0043 | An agent answers where its capability lives (supersedes 0041 every-answerer) | Superseded by 0047 |
| 0044 | Tool approval is a per-conversation policy, resolved per call (auto / ask / deny) | Accepted (design; approval mechanism revised by 0047) |
| 0045 | Playback pause ships opt-in because macOS resume can start unrelated media | Accepted |
| 0046 | A capability-free agent persists finished messages, not live doc streams (scopes 0036) | Superseded by 0047 |
| 0047 | The agent loop runs in the client; tools are dispatched actions, and the daemon provides data, not inference | Accepted (design; core deleted as consumers migrate) |
| 0048 | A conversation's loop is chosen by whether its transcript syncs across peers | Superseded by 0051 |
| 0049 | Inference is its own box; the daemon never infers; the client loop talks to a swappable inference server | Accepted |
| 0050 | The inference contract is OpenAI-compatible Chat Completions; Epicenter's backend is one swappable gateway | Accepted |
| 0051 | There is one agent loop; its store seam chooses persistence, so tab-manager needs no second loop | Accepted (supersedes 0048) |
| 0052 | Shortcut reach is the minimum of command, key, and platform ceilings, never a user toggle | Proposed |
| 0053 | The Epicenter bearer is an audience-scoped credential; auth.fetch attaches it only to its origin | Accepted |
| 0054 | An inference backend is either the metered Epicenter gateway or a custom OpenAI-compatible server | Accepted |
| 0055 | Conversation storage is one canonical table in @epicenter/chat; every chat surface syncs | Accepted |
| 0056 | Local inference is a delegated engine behind the OpenAI-compatible seam; the runtime is a swappable default | Accepted |
| 0057 | Assistant markdown renders as a shared component tree, not a sanitized HTML string | Accepted |
| 0058 | Push-to-talk owns the recording it starts, keyed by the recording's id, not a general lifecycle layer | Accepted |
| 0059 | An inference connection is a capability-orthogonal device endpoint; the model is per-conversation (amends 0054) | Accepted (auth axis amended by 0060) |
| 0060 | An inference connection is a base URL and an optional bearer key; everything non-static is an injected transport (amends 0059) | Accepted |
| 0061 | Local Books serves row-level facts from the mirror, computed reports live, and writes back through one approved verb | Accepted |
| 0062 | Local Books stores OAuth tokens in a single 0600 file | Accepted |
| 0063 | The Local Books mirror is a multi-writer cache made safe by one monotonic write door, not single-writer discipline | Accepted |
| 0064 | The Local Books mirror keeps one realm CDC cursor; table existence is the per-entity init latch | Accepted |
| 0065 | Matter is a standalone disk-as-truth tool; its SQLite mirror is a first-class read-only query surface under epicenter matter | Accepted |
| 0066 | Runtime portability is per-concern injection, not a runtime object | Accepted |
| 0067 | Auth owns the /api/session endpoint; the data client is owner-scoped and receives ownerId at construction | Accepted (amended by 0092) |
| 0068 | Privacy is a deployment, not a product feature; the hosted app carries zero privacy-configuration surface | Accepted |
| 0069 | Epicenter is one runnable program (the star) plus a la carte services addressed by base URL and token | Accepted |
| 0070 | Self-host adds no new ownership or auth mode: single-user is a preset, and only the credential source varies | Superseded by 0075 |
| 0071 | OAuth is hosted-only; a custom instance requires a token | Accepted |
| 0072 | Local Books ships as a standalone CLI; the ADR-0047 daemon surface is deferred behind a verb-core seam | Accepted |
| 0073 | Tools speak MCP natively; Epicenter owns only the transport MCP lacks | Accepted (historical relay-floor channel layer deleted by ADR-0079/0086) |
| 0074 | The secret vault is an owner-scoped synced store encrypted under a server-derived keyring, not a passphrase vault | Accepted |
| 0075 | Self-host is a single-partition instance behind one operator-supplied bearer; multi-tenancy is Cloud-only | Accepted (amended by 0092) |
| 0076 | The relational-auth substrate (Better Auth + Postgres) is a Cloud-only layer; the instance composes neither | Accepted |
| 0077 | Parsed-row memoization belongs to the table; the Svelte adapter is a stateless view | Accepted |
| 0078 | Inference is a URL-addressed connection reached through a direct endpoint or gateway | Accepted (relay-floor tool carrier superseded by ADR-0079/0086) |
| 0079 | Cross-device is two planes: Epicenter syncs the CRDT, the box is reached directly as a URL-addressed connection | Accepted (capability-layer deletion landed 2026-07-02) |
| 0080 | The super app is a desktop host; cross-device is remote access to the session, not a per-app capability plane | Accepted (the desktop-host decision is settled; a hosted session broker for turnkey mobile remote is the open product question) |
| 0081 | Per-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 category | Accepted (per-device reach narrowed to desktops by 0116) |
| 0082 | Local Mail syncs by push-free history.list polling | Accepted (the OAuth-client-identity half moved to 0188) |
| 0083 | apps/email is refused; Local Mail is the only Gmail client | Accepted |
| 0084 | Super Chat's shell is a Bun-hosted local server, not a bundled SPA | Accepted |
| 0085 | A box is a role an addressable endpoint plays, not a distinct node type | Accepted |
| 0086 | There is no live consumer for network-reachable capability reach; opensidian's cross-device tools are superseded by the super app, not migrated | Accepted |
| 0087 | Honeycrisp is the maintained notes product from one isomorphic workspace surface | Accepted (one package contract amended by 0130) |
| 0088 | Sign-in is an enhancement, never a door | Accepted |
| 0089 | The blob store is a presigned-S3 kernel and the bucket is its only index | Accepted (key prefix amended by 0092) |
| 0090 | The blob layer stays plaintext; confidentiality belongs to the encrypting consumer | Accepted |
| 0091 | Blobs trade a file for a durable content-addressed URL; documents are the only manifest | Accepted (URL shape amended by 0092) |
| 0092 | Identity is the partition | Accepted |
| 0093 | KV metadata belongs to the workspace kv namespace | Superseded by 0130 |
| 0094 | The connection is the boot decision: one connect call | Accepted |
| 0095 | WebSocket room auth uses route-owned subprotocol bearers | Accepted |
| 0096 | Local workspace persistence is environment-injected | Accepted |
| 0097 | Super Chat tool modules receive a host API | Superseded by 0111 |
| 0098 | Every Local Mail concept a human acts on round-trips through Gmail API state | Accepted |
| 0099 | Replace Transformations with a Dictionary, an always-on Polish, and a portable Recipe library | Accepted |
| 0100 | AI credits are product-priced units; the charge shape follows whether cost is known before the call | Accepted (STT overspend remedy refined by 0103) |
| 0101 | Native typed-row projection views are earned serially; board is first | Accepted |
| 0102 | Vocab stores verbatim entries under a human-owned note and refuses glosses, SRS, and provenance | Accepted |
| 0103 | STT overspend is bounded by cheap guards in trigger order, never by media preflight or a reservation lock | Accepted |
| 0104 | Hosted models are a build-time seed catalog, not a discovered one; a runtime overlay is a named deferral | Accepted |
| 0105 | Vocab is a multilingual tutor and readings are a client-side derived view | Accepted |
| 0106 | A child-doc body owns one layout; the polymorphic timeline is refused until a product earns it | Accepted (row-body layout amended by 0135) |
| 0107 | A child-doc text body is a plain Y.Text; the timeline array is deleted | Accepted (row-body root amended by 0135) |
| 0108 | Third-party provider credentials are selected by the app's target provider-environment, encoded in the secret name and resolved by one injected helper | Accepted |
| 0109 | Hosted Tauri auth keeps app-owned keyring edges until three real callers earn sharing | Accepted |
| 0110 | Edit write timing follows the value owner; there is no debounce tier | Accepted |
| 0111 | Super Chat v1 exposes built-in Epicenter apps and defers extension surfaces | Accepted |
| 0112 | The CLI watcher is not a callable action server | Accepted |
| 0113 | Super Chat session commands are host-owned; transports only frame them | Accepted |
| 0114 | Hosted auth surfaces are plain tool logins, not marketing pages | Accepted |
| 0115 | Super Chat remote attach rides an endpoint-addressed trusted AttachRelay; one consumer, not the deleted route-addressed relay floor | Accepted |
| 0116 | Local Mail is desktop-first: one Bun engine, no background mail service, the open app owns the sync loop | Accepted |
| 0117 | Global shortcut input is plugin chords only, and the macOS tap is just the paste grant watcher | Accepted |
| 0118 | Epicenter is one trusted Bun-hosted SPA origin | Accepted (app-window HTTP authority amended by 0179) |
| 0119 | Complete record maps sync through schema-blind server-ordered patches | Superseded by 0131 |
| 0120 | Fields validate present values and table lenses own presence | Accepted (read surface amended by 0175) |
| 0121 | Background sync resolves key conflicts by server order | Accepted |
| 0122 | Logical records are portable; SQLite files and views are runtime state | Accepted |
| 0123 | Bounded metadata uses record authority; merge-sensitive state uses lazy child documents | Superseded by 0124 |
| 0124 | Workspace documents are top-level parameterized resources | Superseded by 0130 |
| 0125 | Record definitions are release-local lenses and never migrate user data | Accepted |
| 0126 | Child documents use format capabilities and evolve outside records databases | Superseded by 0124 (touch policy amended by 0128) |
| 0127 | Chat streams live turns in client state and stores finished messages as records | Superseded by 0152 |
| 0128 | Tables do not declare document-edit touch policy without a runtime owner | Accepted |
| 0129 | Matter and Workspace share fields, not authority policy | Accepted |
| 0130 | Workspace definitions expose tables with row-owned documents and a release-local KV lens | Accepted |
| 0131 | Row sync folds sealed RowIntent rounds without refusal | Superseded by 0141 |
| 0132 | Workspace KV is one reserved immortal row | Accepted |
| 0133 | The workspace authority stores documents as sequence-addressed update logs | Superseded by 0141 |
| 0134 | Replicas store confirmed state and compacted RowIntents | Superseded by 0141 |
| 0135 | Row documents have application-owned roots | Accepted |
| 0136 | Replica baseline acquisition uses a disposable anchored live scan | Superseded by 0142 |
| 0137 | Hosted storage is one eventually enforced physical account allowance | Accepted |
| 0138 | Device and account workspace adoption | Superseded by 0139 |
| 0139 | Account runtime open adds device state through native intents | Superseded by 0143 |
| 0140 | Open workspaces synchronize automatically and callers settle one watermark | Accepted |
| 0141 | Authority current state and receipt watermarks drive row convergence | Accepted |
| 0142 | Bootstrap, history gaps, and lineage mismatches have distinct recovery | Accepted |
| 0143 | Account open never consumes Device data | Accepted |
| 0144 | Scalar rows and row documents synchronize through independent client planes | Superseded by 0171 |
| 0145 | One account authority owns every workspace and one socket per open row document | Proposed |
| 0146 | Row documents use one Yjs 14 major and runtime-native update logs | Proposed |
| 0147 | Cross-plane transfer and recovery use logical coordination, not atomic snapshots | Proposed |
| 0148 | Blobs use opaque identifiers rather than content hashes | Superseded by 0173 |
| 0149 | Local blob stores are canonical and remote replication is explicit | Superseded by 0171 |
| 0150 | Whispering uploads operator-readable audio | Accepted |
| 0151 | Local workspace stores use owner-first directories | Accepted |
| 0152 | Epicenter Home is a shell above workspaces | Accepted (installation boundary amended by 0179) |
| 0153 | Trusted apps are source-built static catalog members | Superseded by 0179 |
| 0154 | Blob access is address-only | Accepted |
| 0155 | Epicenter desktop auth is one Bun authority with credential-free windows | Accepted |
| 0156 | Applications bring workspace lenses; runtimes own workspaces by ID | Accepted |
| 0157 | Read-only SQL exposes one schema-opaque row relation | Accepted |
| 0158 | Installed apps declare workspace IDs but run no Bun modules | Superseded by 0160 |
| 0159 | Row documents persist in one owner-side SQLite update log | Accepted |
| 0160 | Lenses interpret durable namespaces without creating lifecycle scopes | Proposed |
| 0161 | Each person has one Epicenter replicated on each adapter boundary | Proposed |
| 0162 | Epicenter Home owns relational inspection; applications receive no SQL | Proposed |
| 0163 | Scalar sync separates fact reads from numbered intent submissions | Proposed |
| 0164 | Scalar facts converge independently; Epicenter refuses distributed transactions | Proposed |
| 0165 | Browser origins contain independent Epicenter replicas | Accepted |
| 0166 | Data, document sync, and agent replace Workspace | Proposed |
| 0167 | A portable Epicenter is an identity-free export of one authority cut | Proposed |
| 0168 | Lenses are complete pure JSON interpretations | Proposed |
| 0169 | Row references are non-enforcing table interpretations | Proposed |
| 0170 | One live Epicenter has sealed Backups; Restore creates a fresh authority lifetime | Proposed |
| 0171 | Every durable local write leaves an automatic authority obligation | Proposed |
| 0172 | SQLite stores convergent facts and documents; raw files store blob bytes | Proposed |
| 0173 | Each row owns at most one write-once immutable blob | Proposed |
| 0174 | Row documents project as nullable compact cells and persist as bounded live chains | Proposed |
| 0175 | Table traversal is complete and classified, with paging kept private | Accepted |
| 0176 | Lenses declare no query capabilities; indexed reads require separate owners | Accepted |
| 0177 | A browser replica is owned by a storage-partition and origin pair | Accepted |
| 0178 | Row facts and value facts are separate relations keyed by structured coordinates | Accepted |
| 0179 | An installed app is an inert built folder admitted through one static-artifact boundary | Accepted (HTTP authority restored by 0185; native commands amended by 0186) |
| 0180 | Epicenter has one host-owned active local transcription model | Accepted |
| 0181 | Every app receives one portable Epicenter capability handle | Accepted |
| 0183 | Epicenter mediates the effects it owns and names the rest unmediated | Accepted (ordinary HTTP observation withdrawn by 0185) |
| 0184 | One host recorder progressively stages each claimable recording until its owner stops or cancels it | Accepted |
| 0185 | Trusted app HTTP uses Tauri's standard transport without observation | Accepted (capability carriers clarified by 0187) |
| 0186 | An app reaches Epicenter through one bundled MIT client it installs itself | Accepted (amends 0181 at delivery and namespace, 0179 at the native command surface; data settled by 0187) |
| 0187 | A bound handle reports staleness; tables can name rows, values cannot | Accepted |
| 0188 | The Gmail application identity belongs to the shipped distribution, and no Epicenter server enters the Gmail path | Proposed (direction locked; flips to Accepted when the official Google client is provisioned and verified) |
When you add an ADR, add its row here.