diff --git a/01-RESEARCH/011-the-module-graph/worked-provider.md b/01-RESEARCH/011-the-module-graph/worked-provider.md index 838b9a3..d7da442 100644 --- a/01-RESEARCH/011-the-module-graph/worked-provider.md +++ b/01-RESEARCH/011-the-module-graph/worked-provider.md @@ -159,40 +159,66 @@ case, and reaching it is not the host's job. So the same operation is local at b remote afterwards, which is either two mechanisms or one mechanism with a boundary crossing in it. Undecided, and it is the sharpest unresolved thing in this file. -## Several modules, one database +## Several modules, one database — and the case for refusing -Asked directly, and it is three different needs wearing one sentence. They want different -answers and one of them collides with a rule already in force. +Asked, then reconsidered by the operator: *maybe we should not allow it.* -**A grant is not always a whole database.** A provider offers *kinds* of grant, and the kind is -what separates these: +The permissive version was a per-consumer **schema** inside a shared database — its own +namespace, its own migrations, revocable by dropping the schema, with a cross-context join +possible but deliberate. -| The need | The grant | -|---|---| -| my own tables, nobody else's business | a **database** | -| read what another module holds | a **read-only role** on an existing database | -| my own tables *inside* a database others also use | a **schema** within it — a namespace the consumer owns | +**The stricter version is better, and it goes further than schemas.** -The third is the interesting one, and the one asked about. +> **A module is only ever granted a resource it exclusively owns.** -**Loose tables in a shared database is what `how-we-build` §4 warns against**, in as many words: -*contexts integrate through the record, never through a shared schema — today several domains -share one forty-five-table schema, which is why work belonging to one context keeps having to be -implemented in another.* That is not a style objection; it is the observed cost, already paid. +No shared writes. And **no read-only role on another module's database either** — reading +another context's tables couples you to its layout exactly as firmly as writing them does, and +the coupling is harder to see because nothing breaks until the owner changes a column. -**A per-consumer schema inside a shared database keeps what the request wants and drops what §4 -objects to.** The consumer gets tables in the same database — same connection, same backup, and -a cross-schema read is physically possible when it is genuinely needed. What it also gets is -ownership: its migrations touch its own namespace, two modules cannot collide over a table name, -and revoking the grant drops the schema rather than guessing which tables belonged to whom. +That is what [`how-we-build`](../../00-META/how-we-build.md) §4 already says: *contexts integrate +through the record, never through a shared schema.* The permissive version kept the letter of it +and left the temptation in place, and the path of least resistance wins eventually. A boundary +that is merely inconvenient to cross is a boundary that gets crossed. -Which leaves the fault §4 names — *joining across a boundary* — possible but no longer -accidental. That is the honest position: physically co-located, logically owned, and a -cross-context join is now a thing somebody has to deliberately write rather than the path of -least resistance. +### What it costs -**And it makes revocation answerable**, which the whole-database version was not. Uninstalling a -consumer drops its schema. Nothing has to work out which of forty-five tables were whose. +**Cross-module reporting.** Anything wanting to know what several modules hold can no longer +join across them. It consumes their events, or calls their interface, and neither is as +immediate as a query. + +That cost is the point rather than a regrettable side effect — it is §4's whole argument, and +the mesh already has both mechanisms: an event stream every context publishes to, and a tool +surface every module exposes. What gets harder is the thing that was making work belonging to +one context keep having to be implemented in another. + +**One more connection per consumer.** A dozen modules means a dozen databases rather than a +dozen schemas in one. For a relational store this is unremarkable; it is worth stating only so +nobody discovers it as a surprise. + +### What it deletes + +The effort has been looking for what the design *removes* rather than adds, and this is the +first clear instance: + +- **Grant kinds.** There is one — an exclusive resource. No schema grants, no read roles, no + scoping rules for who may see what inside a shared thing. +- **The question of who owns which table**, and with it the guessing at revocation time. +- **Cross-module migration ordering.** Two modules migrating one database need their migrations + ordered against each other. Exclusive ownership means a module's migrations are ordered only + against itself. +- **A whole class of permission modelling** that a shared store would otherwise need. + +### What it does not answer + +**The mesh's own registry is read by many things.** Under this rule they cannot read its tables, +so they consume its events or call its tools. That is achievable and it is a real change from +how the mesh works today, where reading the registry directly is ordinary — and it is the same +change [ADR 0037](../../02-DECISIONS/0037-the-host-applies-it-does-not-decide.md) already forces +on the host for unrelated reasons. + +Whether *every* consumer of the registry can be served by events and an interface is not +established here. It is the one thing that could make this rule unworkable, and it should be +checked against real consumers before the rule is recorded as a decision. ## A migration belongs to the consumer and runs on the provider