Files
hq/02-DECISIONS/0125-the-bus-is-the-only-broker.md
jschoubben ce6ae943b7 Merge main: renumber this branch's records around the trunk's
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.
2026-09-27 18:23:41 +02:00

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

  1. 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.
  2. 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.
  3. 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-forwarder moves 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 EVENTS stream 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-broker stops declaring what it delivers. A requirement for amqp then 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 requires line for it. A lab bed: a module declaring only emits and consumes reaches 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.

References

  • ADR 0106 — the bus is NATS; corrected here on what the compatibility broker serves.
  • ADR 0116 — the steps; the conversions land in step 4.
  • ADR 0043 — the scoping that makes one bus safe.
  • design 25, design 27 — the two documents this changes.