Both lines of work numbered from the same point, so four decision records and one design document existed twice with different content. The trunk keeps its numbers and this branch yields — the only rule that scales, because the trunk's are already cited by what merged before them. 0117 the bus is the only broker -> 0125 0118 a module declares its own seats -> 0126 0119 amqp is a provision, not the bus -> 0127 0120 the mesh bus is required -> 0128 0123 a seat carries its role's protocol -> 0129 0124 the predecessor is ending -> 0130 design 29, what a module declares -> design 32 Applied to the code repositories too, because a stale reference is worse when numbers collide than when they dangle: the reader lands on a real record that decided something else. Two reconciliations the merge forced, both real: **0110 was marked wholly superseded and was not.** Its successor says in as many words that everything 0110 decided about what a seat *is* stands untouched — and two records that landed on the trunk rest on exactly that part. So it is accepted again, extended rather than replaced, with a note saying which of its claims moved and where. **A seat's protocol becomes columns, not fields.** The trunk moved the seat set out of compiled code into a table the controller owns. This branch had added what a role accepts, emits and serves to the Go slice. The decision is unaffected and the mechanism is better for it: giving a role a protocol is now a write rather than a rebuild, which is the trunk's own argument applied to what this branch added. One check still fails and it fails on main too: a record resting on ADR 0112 while that is still 'proposed'. Left alone — it is not this merge's to answer.
7.8 KiB
topic, status, superseded-by, date, deciders, reconstructed, extends
| topic | status | superseded-by | date | deciders | reconstructed | extends |
|---|---|---|---|---|---|---|
| the mesh | superseded | 02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md | 2026-09-26 | jochen | false | 02-DECISIONS/0106-the-bus-is-nats.md |
125. The bus is the only broker
Context
ADR 0106 moved the mesh's bus to NATS and kept the AMQP broker "as a module with one purpose — the predecessor's clients", retiring with the last of them. Design 25 repeats that: a compatibility module with a single purpose and a retirement condition.
It is not single-purpose, and was not when that was written. Two modules of the new mesh
declare requires: ["amqp"] and are answered by the broker module's own provisioner:
amqp-ping, whose source says it "exists to PROVE the grant end to end: the mesh gave it a scoped login and a vhost of that name on the lavinmq provider";amqp-email-forwarder, which uses it for work.
What that provisioner answers is not the mesh's bus. Its own comment draws the line: a consumer gets "its own message broker, isolated from every other consumer's by the vhost boundary… a broker of its own, not a shared account on the mesh's control-plane broker" — vhost-per-login, "the exact analog of postgres's database-per-login."
So two different things wear the word broker: the mesh's nervous system, and a private message broker handed to a module as a resource, the way a database is. The first is being replaced. The second was never examined, and on the retirement condition ADR 0106 sets, it disappears with no successor and nothing notices — a module of the new mesh left requiring something no provider answers.
The operator's direction, asked at the point this surfaced: NATS is the heart of the application — not a component it contains, and not a thing to reproduce the predecessor's shapes on.
Considered Options
- Carry the private broker forward onto NATS — each requiring module gets its own NATS account, provisioned like a database. Rejected on three counts. It reproduces the predecessor's shape on the new bus, which is the thing this whole move exists to stop. It gives the mesh two messaging models, so "how does a module send a message" has two answers depending on a manifest line. And NATS accounts isolate subject spaces entirely: a module inside its own account cannot reach the mesh's bus at all, so it would hold two connections and two identities to do one job.
- Keep the compatibility broker indefinitely for the mesh's own modules. Rejected: its retirement condition is the point of it. A module of the new mesh depending on the retired one keeps the predecessor alive permanently, which is the opposite of a compatibility module.
- One bus. A module's messaging is subjects on it, scoped by what it declares. Adopted.
Decision
The bus is the only broker. NATS is the mesh's one messaging system, and every module's
messaging is subjects on that bus under its own account, scoped by its emits and consumes
(ADR 0043). There is no second
broker, and none is handed to a module as a resource.
The amqp interface is not carried forward. It leaves the set of things a module may require
and retires with the compatibility broker rather than gaining a successor.
Concretely, in the controller's seat table: the mesh-broker seat delivers nothing. It
currently reads Delivers: "amqp" — the seat's holder answers a requirement for a broker — and
under this decision it joins mesh-controller and the-catalogue, the foundation seats that
deliver no provision at all. The bus is not something a module asks for; it is what a module is
reached through.
amqp-email-forwardermoves to the bus like any module: what it emits and consumes, declared, and the account follows.amqp-ping's purpose is kept and its mechanism is not. Proving end to end that a module receives scoped messaging it did not configure itself is worth a probe; it becomes a probe of the bus, and its assertion changes from "I reached my own vhost" to "I reached exactly my subjects and was refused the rest."
A module that wants a queue of its own has one already: a subject nothing else may publish to and a durable consumer of its own, both derived from its declaration. What it does not get is a server of its own.
The mesh's own streams are the controller's, created at genesis, not provisioned — and
EVENTS is one stream, closing the question design 25
§11 left open. The reason is not preference but bootstrapping: a provisioner is a module, and
a module needs a bus account before it can run at all. Anything the bus itself is made of must
exist before the first module starts, so it is composed as configuration
(ADR 0106: never through a management API) rather than provisioned by
something that could not yet be running.
Consequences
- Design 25 gains the distinction and loses the "single purpose" claim; its §11 question about
the
EVENTSstream closes here. - Nothing in design 27's model changes — the four provider kinds, the contract, resolution all stand, and it never enumerated interfaces, so there is nothing to strike from it. What changes is that messaging leaves the set of things resolved at all: every module has it by existing.
- One line of the controller's seat table changes, and it is the load-bearing one:
mesh-brokerstops declaring what it delivers. A requirement foramqpthen resolves to nothing and is refused at assignment, which is how the two modules below are found rather than discovered at runtime. - Two modules have conversion work, and it belongs to step 4 of ADR 0116, with the flows. Neither blocks step 1.
- The compatibility broker becomes what ADR 0106 already called it — single-purpose — once those two have moved. That record's claim was wrong when written and is made true by this one.
- What got harder: a module that genuinely wanted an isolated server — a tenant boundary at the broker rather than at the subject — no longer has that option, and would have to argue for it as a new decision. That is the intended cost: one bus is the point.
How it is checked
- A module's messaging works with no
requiresline for it. A lab bed: a module declaring onlyemitsandconsumesreaches its subjects, and is refused every other — which is ADR 0116 step 1's permission bed, already required. - Nothing requires
amqp. With the seat delivering nothing, a module still declaring it is refused at resolution — the existing "requirement no provider answers" path, not a new check. A catalogue test asserts no module declares it once the two have moved. - The probe proves the claim it is named for.
amqp-ping's successor fails if a module can reach a subject outside its declaration, not merely if it cannot reach its own. - The compatibility broker's retirement condition can actually be met. A check that no module of the mesh — as opposed to a predecessor client — holds a connection to it.