The bus is the only broker; step 1 starts

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.
This commit is contained in:
2026-09-26 19:26:07 +02:00
parent db4ca9b043
commit 814c9e563f
6 changed files with 177 additions and 9 deletions
+5 -2
View File
@@ -299,8 +299,11 @@ def check_progressive_insights(failures, records):
unmarked change stands out as the anomaly it is. unmarked change stands out as the anomaly it is.
""" """
phrase = re.compile(r"progressive insight", re.I) phrase = re.compile(r"progressive insight", re.I)
marker = re.compile(r"\*\*Progressive insights?\s*[\u2014\u2013-]\s*(\d{4}-\d{2}-\d{2})\.?\*\*") # Both patterns stay on one line: a bold run does not span paragraphs, and `[^*]*` across
loose = re.compile(r"\*\*[^*]*[Pp]rogressive insights?[^*]*\*\*") # newlines will happily join an unrelated `**` far above to the marker below, reporting the
# whole span between them. It did exactly that the first time this ran.
marker = re.compile(r"\*\*Progressive insights?[ \t]*[\u2014\u2013-][ \t]*(\d{4}-\d{2}-\d{2})\.?\*\*")
loose = re.compile(r"\*\*[^*\n]*[Pp]rogressive insights?[^*\n]*\*\*")
iso = re.compile(r"^\d{4}-\d{2}-\d{2}$") iso = re.compile(r"^\d{4}-\d{2}-\d{2}$")
for number, record in sorted(records.items()): for number, record in sorted(records.items()):
+13
View File
@@ -75,6 +75,19 @@ after its deliveries are exhausted; a module's account cannot publish outside it
subscribe outside its `consumes`. Then the cutover bed: a mesh on AMQP with the predecessor's subscribe outside its `consumes`. Then the cutover bed: a mesh on AMQP with the predecessor's
compatibility broker beside it moves its bus in one rollout with every node reporting afterwards. compatibility broker beside it moves its bus in one rollout with every node reporting afterwards.
## Progressive insight
> **Progressive insight — 2026-09-26.** *The compatibility broker was not single-purpose when this
> was written.* This record says the adopted AMQP broker is "kept as a module with one purpose —
> the predecessor's clients". Two modules of the new mesh also depended on it, through a `requires:
> ["amqp"]` grant its provisioner answered with a private vhost — `amqp-ping` and
> `amqp-email-forwarder`. On the retirement condition below, both would have been left requiring
> something no provider answers.
> [ADR 0117](0117-the-bus-is-the-only-broker.md) resolves it by moving them onto the bus and
> retiring the interface, which makes this record's sentence true rather than merely intended. The
> decision — the bus is NATS, the AMQP broker becomes the predecessor's compatibility broker and
> retires with the last of them — is unchanged.
## References ## References
- [research 014](../01-RESEARCH/014-the-bus-on-nats/00-overview.md) - [research 014](../01-RESEARCH/014-the-bus-on-nats/00-overview.md)
@@ -0,0 +1,132 @@
---
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.
+1
View File
@@ -133,6 +133,7 @@ python3 00-META/checks/index.py fail if stale
- **0105** — [The mesh adopts the predecessor's tunnel in place](0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md) - **0105** — [The mesh adopts the predecessor's tunnel in place](0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md)
- **0106** — [The bus is NATS](0106-the-bus-is-nats.md) - **0106** — [The bus is NATS](0106-the-bus-is-nats.md)
- **0116** — [The bus is built in five steps, and the protocol moves with it](0116-the-bus-is-built-in-five-steps.md) - **0116** — [The bus is built in five steps, and the protocol moves with it](0116-the-bus-is-built-in-five-steps.md)
- **0117** — [The bus is the only broker](0117-the-bus-is-the-only-broker.md)
### Its tiers, from the bottom up ### Its tiers, from the bottom up
+20 -5
View File
@@ -1,6 +1,6 @@
--- ---
layer: to-be layer: to-be
status: proposed status: in-progress
code: code:
- mesh-controller internal/link (to be replaced) - mesh-controller internal/link (to be replaced)
- mesh-host internal/link (to be replaced) - mesh-host internal/link (to be replaced)
@@ -10,6 +10,7 @@ code:
updated: 2026-09-26 updated: 2026-09-26
decisions: decisions:
- 02-DECISIONS/0106-the-bus-is-nats.md - 02-DECISIONS/0106-the-bus-is-nats.md
- 02-DECISIONS/0117-the-bus-is-the-only-broker.md
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md - 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md - 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
- 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md - 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
@@ -188,8 +189,19 @@ current. Nothing is declared as `reload-on` or `restart-on` for this resource at
Its guard is the same rule as the AMQP broker's: the monitoring port is refused from anything but Its guard is the same rule as the AMQP broker's: the monitoring port is refused from anything but
the private network. It is raised at genesis like the store, adopted as a module in the same the private network. It is raised at genesis like the store, adopted as a module in the same
phase. The predecessor's AMQP broker remains a module of its own, `lavinmq-compat`, with a single phase. The predecessor's AMQP broker remains a module of its own, `lavinmq-compat`, with a
purpose and a retirement condition: no client connected for a period the operator sets. retirement condition: no client connected for a period the operator sets.
**And with one purpose, which it did not have when this was written.** Revision, second review
([ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md)): two modules of the new mesh
required a broker *of their own* from it — a private vhost per consumer, the analog of
database-per-login, which is a different thing from the mesh's bus and was never examined here.
**There is one bus, and no module is handed a broker as a resource.** A module's messaging is
subjects on the bus under its own account, scoped by its `emits` and `consumes`; a module that
wants a queue of its own has a subject nothing else may publish to and a durable consumer, both
from its declaration. What it does not get is a server of its own. The `amqp` interface retires
with the compatibility broker instead of gaining a successor, and the two modules move to the bus
in step 4.
## 6. Joining: the enrolment handshake ## 6. Joining: the enrolment handshake
@@ -398,8 +410,11 @@ find what changed and why.
**Still open:** **Still open:**
- Whether EVENTS should be one stream or one per emitting module (retention per module vs. one - ~~Whether EVENTS should be one stream or one per emitting module.~~ **Closed**
policy). One stream is proposed; the review may disagree. ([ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md)): one stream, and not as a
preference — the streams the bus is made of are composed as configuration before any module
runs, because a provisioner is itself a module that needs a bus account to start. Bootstrapping
decides it.
- The heartbeat interval and the controller's "quiet" threshold on core NATS without persistence — - The heartbeat interval and the controller's "quiet" threshold on core NATS without persistence —
the same numbers as today are proposed. the same numbers as today are proposed.
- Whether the person's client is a catalogue module (runs on an enrolled workstation node) or a - Whether the person's client is a catalogue module (runs on an enrolled workstation node) or a
+6 -2
View File
@@ -1,11 +1,15 @@
--- ---
layer: to-be layer: to-be
status: proposed status: in-progress
code: [] code:
- mesh-catalog modules/nats
- mesh-controller internal/catalogue
- mesh-lab scenarios
updated: 2026-09-26 updated: 2026-09-26
decisions: decisions:
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md - 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
- 02-DECISIONS/0106-the-bus-is-nats.md - 02-DECISIONS/0106-the-bus-is-nats.md
- 02-DECISIONS/0117-the-bus-is-the-only-broker.md
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md - 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
- 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md - 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md