A module does declare requirements the provisioner fulfils — but the broker it gets that way is a private vhost, the analog of a database, not the mesh's bus. Two modules of the new mesh depend on it, so the compatibility broker was never single-purpose and its retirement would have stranded them. NATS is the heart: one bus, a module's messaging is subjects on it scoped by what it declares, and no module is handed a server of its own. The seat delivers nothing; the interface retires with the broker. Also closes the EVENTS question — one stream, on the bootstrap argument, not preference. Designs 25 and 28 go in-progress: step 1 is starting. The insight check caught a false positive on its own first real use — its bold-run pattern crossed newlines and joined an unrelated `**` to the marker. Constrained to one line, still catching all four bad shapes.
133 lines
7.7 KiB
Markdown
133 lines
7.7 KiB
Markdown
---
|
|
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.
|