From aa767d17a89630f839cf6fe24a2f8451d56ff686 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 26 Aug 2026 23:09:44 +0200 Subject: [PATCH] 011: a module is granted only what it exclusively owns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reconsidered by the operator — maybe shared databases should not be allowed at all — and the stricter version is better and goes further than the schemas it replaces. 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. That is how-we-build §4 taken at its word rather than at its letter. The permissive version — a per-consumer schema, revocable, with cross-context joins possible but deliberate — kept the letter and left the temptation. A boundary that is merely inconvenient to cross is a boundary that gets crossed. The cost is cross-module reporting, and it is the point rather than a regrettable side effect: anything wanting to know what several modules hold consumes their events or calls their interface. That is §4's whole argument, and the mesh already has both mechanisms. What gets harder is precisely the thing that was making work belonging to one context keep having to be implemented in another. And it is the first clear instance of what this effort has been hunting — what the design DELETES rather than adds. Grant kinds collapse to one: an exclusive resource. With them go the question of who owns which table, the guessing at revocation time, cross-module migration ordering, and a class of permission modelling a shared store would otherwise need. One thing it does not answer, recorded because it could make the rule unworkable: the mesh's own registry is read directly by many things today, and under this rule they consume events or call tools instead. Achievable in principle. Whether EVERY current consumer can be served that way is unchecked, and should be before this becomes a decision. --- .../011-the-module-graph/worked-provider.md | 78 ++++++++++++------- 1 file changed, 52 insertions(+), 26 deletions(-) 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