Reconcile: adopt initialization's consolidated HQ as canonical, re-home this session's new work #24

Merged
jschoubben merged 177 commits from reconcile-init-into-main into main 2026-09-05 10:27:11 +00:00
Showing only changes of commit aa767d17a8 - Show all commits
@@ -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