--- topic: the mesh status: accepted date: 2026-09-26 deciders: jochen reconstructed: false extends: 02-DECISIONS/0106-the-bus-is-nats.md --- # 117. The bus is the only broker ## Context [ADR 0106](0106-the-bus-is-nats.md) 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](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) 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](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md)). 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](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §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](0106-the-bus-is-nats.md): 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](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md)'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](0116-the-bus-is-built-in-five-steps.md), 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](0116-the-bus-is-built-in-five-steps.md) 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](0106-the-bus-is-nats.md) — the bus is NATS; corrected here on what the compatibility broker serves. - [ADR 0116](0116-the-bus-is-built-in-five-steps.md) — the steps; the conversions land in step 4. - [ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md) — the scoping that makes one bus safe. - [design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md), [design 27](../03-DESIGN/01-to-be/27-a-module-requires-the-mesh-resolves.md) — the two documents this changes.