0117 went a step further than it had grounds for. It was right that the bus is the only bus, and wrong that the amqp interface must therefore retire — because it conflated two reasons to want a broker. Using one to reach another module is a second bus and stays refused. Needing an AMQP broker as a backing service, the way something needs a database, is ordinary, and forbidding it would make the mesh unable to run normal software while calling that architecture. So the broker becomes a plain provider module: no seat, not foundation, never raised at genesis, no retirement condition. lavinmq now claims nothing and provides amqp; nats claims mesh-broker and provides nothing. The rule that survives is about direction, not software: inter-module communication goes over the bus. A module may hold a broker for itself; it may not use one as a channel to another module. That is a review judgement where 0117 could have used a parser, which is the honest cost. 0106's progressive insight was itself wrong and is corrected by a second one there — nothing moves off the old broker, so its "one purpose" sentence does not become true, it is just not what that server is. The insight check needed two fixes it found itself: a date may carry trailing words, and a bold run with a link is discussing an insight rather than marking one. All four bad shapes still fire.
7.8 KiB
topic, status, superseded-by, date, deciders, reconstructed, extends
| topic | status | superseded-by | date | deciders | reconstructed | extends |
|---|---|---|---|---|---|---|
| the mesh | superseded | 02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md | 2026-09-26 | jochen | false | 02-DECISIONS/0106-the-bus-is-nats.md |
117. 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.