--- topic: the tiers status: accepted date: 2026-08-26 deciders: jochen reconstructed: false extends: 0009-modules-and-the-graph.md --- # 8. A context owns its store, exclusively ## Context [ADR 0009](0009-modules-and-the-graph.md) settles what a module declares. This settles what a grant may be, and it is the half that **removes** things. `how-we-build` §4 already says *contexts integrate through the record, never through a shared schema*, and states the cost: several domains share one forty-five-table schema, which is why work belonging to one context keeps having to be implemented in another. That was written as a principle. Counted, it is thirteen foreign tables belonging to three separate contexts, living in the mesh's own registry database. ## Considered options 1. **A schema per consumer inside a shared database.** Namespaced, revocable by dropping the schema, with a cross-context join possible but deliberate. Rejected: it keeps the letter of §4 and leaves the temptation in place, and a boundary that is merely inconvenient to cross gets crossed. 2. **Read-only roles on another context's store.** Rejected for the same reason and one worse: reading another context's tables couples you to its layout exactly as firmly as writing them, and the coupling is invisible until the owner changes a column. 3. **Exclusive ownership.** Chosen. ## Decision > **A context is granted only what it exclusively owns.** No shared writes. No read-only role on another context's store. If you need what another context holds, you ask it or you subscribe to it. **The unit is the context, not the process.** Everything inside a context — its service, its surface, its tools — reads its own store freely. A board showing the mesh's own nodes and modules is the mesh showing its own data, not a boundary crossing. What is forbidden is a *different* context reading it. ### Asking or subscribing is derived, not chosen [ADR 0004](0004-a-node-and-how-it-joins.md) makes disconnection an ordinary situation. So: - **Anything that must keep working while disconnected cannot ask** — there is nobody to ask. It keeps a local copy, which means subscribing. - **Anything where a stale answer is worse than none cannot subscribe.** A display may lag; a decision about whether a grant is still valid may not. Neither is a query against another store, whatever transport it travels over. ### What that is, concretely *Written 2026-08-29, on building the first one — the rule above was clear and what to type was not.* **One PostgreSQL database per context, named for the context.** A separate database rather than a separate schema is the whole point: a cross-schema join is a qualified name away, and a cross-database join needs a foreign data wrapper somebody has to install and explain. **And one credential per context, held only by it.** There is no mesh-wide connection setting and no way to ask for one, so reaching another context's store is not a matter of restraint — a process has no address for it and nothing to present. That is also how this rule is *checked*: what a context can reach is the list of variables the declaration running it grants, and it is read there rather than audited in code. **Contexts that do not exist yet do not get a database.** The bootstrap creates the ones there are. **How a context added later gets its database is open**, and it is a real question: by then there is a control plane, but a control plane holding a credential that can create databases is holding rather more than the thing it exclusively owns. ## What this removes The first clear list of what the design deletes rather than adds: - **Grant kinds.** There is one: an exclusive resource. No schema grants, no read roles, no rules about who may see what inside a shared thing. - **The question of who owns which table**, and the guessing at revocation time. Removing a consumer drops what it was granted, whole. - **Cross-context migration ordering.** Two contexts migrating one database must be ordered against each other. Exclusive ownership means a context's migrations are ordered only against itself. - **A class of permission modelling** a shared store would otherwise need. ## Consequences - **Cross-context reporting is harder, and that is the point.** Anything wanting to see across contexts consumes their events or calls their interfaces. That is §4's argument, and the cost it names is the one already paid. - **A single surface over several contexts still works** — that is what a surface is. It reads interfaces, not stores. This holds while the contexts sit behind **one** interface; splitting a context into its own deployable costs that, and the composition would have nowhere to live that tier 3 permits. **A real constraint on how far the control plane may be split.** - **Three contexts must move out of the registry database**, taking thirteen tables with them. Their dependency on the registry then shrinks to almost nothing — one of them needs a single table. - **The node appliers were already handled.** [ADR 0005](0005-the-node-host.md) stopped the host querying the mesh database for tier reasons unrelated to this, and it removes most of the remaining direct readers as a side effect. - **What a consumer does about events missed while disconnected is not decided** — replay from a point, ask once and resume, or rebuild. The question every projection has. ## References - [`how-we-build.md`](../00-META/how-we-build.md) §4 — the rule this makes enforceable. - [Research 011](../01-RESEARCH/011-the-module-graph/worked-provider.md) — the count, the worked provider, and the dashboard case. - [ADR 0004](0004-a-node-and-how-it-joins.md) — why asking or subscribing is derived.