AMQP is a provision, not the bus

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.
This commit is contained in:
2026-09-26 21:08:40 +02:00
parent 7abb268de6
commit 85f972749a
9 changed files with 148 additions and 12 deletions
+7 -1
View File
@@ -302,7 +302,9 @@ def check_progressive_insights(failures, records):
# Both patterns stay on one line: a bold run does not span paragraphs, and `[^*]*` across # Both patterns stay on one line: a bold run does not span paragraphs, and `[^*]*` across
# newlines will happily join an unrelated `**` far above to the marker below, reporting the # 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. # 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})\.?\*\*") # Trailing words after the date are allowed — "— 2026-09-26, correcting the one above." — so
# an insight can say what it relates to. Only the date's presence and position are fixed.
marker = re.compile(r"\*\*Progressive insights?[ \t]*[\u2014\u2013-][ \t]*(\d{4}-\d{2}-\d{2})[^*\n]*\*\*")
loose = re.compile(r"\*\*[^*\n]*[Pp]rogressive insights?[^*\n]*\*\*") 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}$")
@@ -316,6 +318,10 @@ def check_progressive_insights(failures, records):
for m in loose.finditer(text): for m in loose.finditer(text):
if any(s <= m.start() and m.end() <= e for s, e, _ in good): if any(s <= m.start() and m.end() <= e for s, e, _ in good):
continue continue
# A bold run carrying a link is discussing an insight — usually another record's —
# rather than marking one. A marker never needs to cite anything.
if "](" in m.group(0):
continue
failures.add("insights", rel(record["path"]), failures.add("insights", rel(record["path"]),
"a progressive insight is not in the dated marked form " "a progressive insight is not in the dated marked form "
"'**Progressive insight \u2014 YYYY-MM-DD.**': %s" % m.group(0)) "'**Progressive insight \u2014 YYYY-MM-DD.**': %s" % m.group(0))
+11
View File
@@ -88,6 +88,17 @@ compatibility broker beside it moves its bus in one rollout with every node repo
> decision — the bus is NATS, the AMQP broker becomes the predecessor's compatibility broker and > decision — the bus is NATS, the AMQP broker becomes the predecessor's compatibility broker and
> retires with the last of them — is unchanged. > retires with the last of them — is unchanged.
> **Progressive insight — 2026-09-26, correcting the one above.** *The broker is not a
> compatibility module at all, and the sentence does not become true.* The insight above said
> [ADR 0117](0117-the-bus-is-the-only-broker.md) would make "one purpose — the predecessor's
> clients" true by moving the mesh's own modules off it.
> [ADR 0119](0119-amqp-is-a-provision-not-the-bus.md) supersedes that: nothing moves off, because
> a module may legitimately need an AMQP broker as a backing service the way it needs a database.
> The broker becomes **an ordinary provider module** — no seat, not foundation, not raised at
> genesis, and with no retirement condition, because the day its last client disappears is not a
> day anything is waiting for. What this record decided — the mesh's bus is NATS — is untouched
> by both; what was wrong was the sentence describing what happens to the old server, twice.
## 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)
@@ -1,6 +1,7 @@
--- ---
topic: the mesh topic: the mesh
status: accepted status: superseded
superseded-by: 02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md
date: 2026-09-26 date: 2026-09-26
deciders: jochen deciders: jochen
reconstructed: false reconstructed: false
@@ -0,0 +1,103 @@
---
topic: the mesh
status: accepted
date: 2026-09-26
deciders: jochen
reconstructed: false
supersedes: 02-DECISIONS/0117-the-bus-is-the-only-broker.md
---
# 119. AMQP is a provision, not the bus
## Context
[ADR 0117](0117-the-bus-is-the-only-broker.md) decided that the bus is the only broker, and went
one step further than it had grounds for: it also decided that the `amqp` **interface** — a module
requiring a message broker of its own — "is not carried forward" and "retires with the
compatibility broker rather than gaining a successor", with the two modules declaring it converted
to the bus in step 4.
The operator's correction: **AMQP is deprecated as the mesh's transport, not abolished as a
service.** The broker module keeps running and keeps answering `amqp` requirements. It is no
longer a core part of the mesh — *"it's just a module like mssql now."*
**What 0117 conflated** is two different reasons a module might ask for a broker, which look
identical in a manifest:
1. **To talk to other modules.** Wrong under one bus, and the thing 0117 was right to refuse: a
private broker used as inter-module transport is a second bus, with every guarantee crossing a
seam and no scoping the mesh can see.
2. **Because it genuinely needs an AMQP broker**, the way something needs a database — a queue for
its own internals, or interop with software that speaks AMQP and nothing else. That is a
backing service, and the mesh has a word for backing services already.
0117 saw the first and legislated against both. The second is ordinary, and forbidding it would
make the mesh unable to run a large class of perfectly normal software while claiming that as
architecture.
## Considered Options
1. **Keep 0117 as written** — retire the interface, convert the two modules. Rejected by the
operator, and wrongly reasoned besides: it treats "needs an AMQP broker" as always a mistake.
2. **Keep the broker as the predecessor's compatibility module**, as ADR 0106 framed it, with a
retirement condition. Rejected: it is not single-purpose and its clients are not only the
predecessor's, so the retirement condition describes a day that will not come.
3. **The broker is an ordinary provider module of an ordinary provision.** Adopted.
## Decision
**The mesh's bus is NATS and only NATS.** Everything 0117 decided about *the bus* stands: one bus,
a module's messaging is subjects on it scoped by what it declares, no module is handed a bus of
its own, and the `mesh-broker` seat is the NATS server's.
**`amqp` remains a provision a module may require**, answered by the broker module the way
`postgres-database` is answered by the store module or a database is answered by mssql. It is not
deprecated as an interface; the software behind it is simply no longer the mesh's nervous system.
**The broker module stops being foundation.** It claims no seat — `mesh-broker` is the NATS
server's — it is not raised at genesis, nothing in the mesh requires it, and a mesh that never
installs it is a complete mesh. It is installed when something wants it, like any other provider.
**The rule that survives, stated so it can be applied:** *inter-module communication goes over the
bus.* A module may hold a broker, a database or a cache as a backing service; it may not use one
as a channel to another module. The line is not which software is involved, it is whether a second
module is on the other end.
**Neither `amqp-ping` nor `amqp-email-forwarder` needs converting.** 0117 put that work in step 4;
it is removed. They require a backing service and a provider answers.
## Consequences
- **The "compatibility broker" framing is wrong and goes.** There is no `lavinmq-compat`, no
single purpose and no retirement condition. Design 25 §5 is corrected.
- **[ADR 0106](0106-the-bus-is-nats.md)'s progressive insight was itself wrong** and is corrected
by a second one there. It said 0117 would make 0106's "one purpose — the predecessor's clients"
sentence true by moving the mesh's modules off. Nothing moves off; the sentence is simply not
what the broker is.
- **The seat change stands**, for a better reason than 0117 gave: not because a broker cannot be
provisioned, but because *this* broker is not the mesh's bus. The broker module drops its
`mesh-broker` claim and the `nats` module takes it.
- **Step 4 loses two conversions**; step 1 and the WBS are otherwise unaffected.
- **What got harder:** the rule is now a judgement rather than a prohibition. "Is this a backing
service or a channel to another module?" has to be asked in review, where 0117 could have
answered it with a parser. That is the honest cost of allowing the legitimate case.
## How it is checked
- **A module's own messaging needs no `requires`.** The check from 0117, unchanged: a module
declaring only `emits` and `consumes` reaches its subjects and is refused every other.
- **The broker holds no seat.** A manifest test: the broker module claims nothing, and a mesh
raised without it is complete — genesis names it nowhere.
- **`amqp` resolves like any provision.** A resolution test: a module requiring it is answered by
the provider, refused when none is assigned, and neither case touches the bus.
- **What cannot be checked mechanically**, and is said rather than implied: that a module holding
a broker is not using it to reach another module. Review, not a parser.
## References
- [ADR 0117](0117-the-bus-is-the-only-broker.md) — superseded; its ruling on the bus is kept
whole and only its ruling on the interface is reversed.
- [ADR 0106](0106-the-bus-is-nats.md) — the bus is NATS; its compatibility-broker framing is
corrected here.
- [ADR 0118](0118-a-module-declares-its-own-seats.md) — seats, including the one the NATS server
now holds alone.
+2 -1
View File
@@ -133,7 +133,8 @@ 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) - **0117** — [The bus is the only broker](0117-the-bus-is-the-only-broker.md) *(superseded)*
- **0119** — [AMQP is a provision, not the bus](0119-amqp-is-a-provision-not-the-bus.md)
### Its tiers, from the bottom up ### Its tiers, from the bottom up
+19 -5
View File
@@ -10,7 +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/0119-amqp-is-a-provision-not-the-bus.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
@@ -214,11 +214,25 @@ 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 phase.
retirement condition: no client connected for a period the operator sets.
**The AMQP broker is an ordinary module, not a compatibility layer.** Revision, second review
([ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md)): earlier text here, and
ADR 0106 before it, called it `lavinmq-compat` — one purpose, the predecessor's clients, and a
retirement condition of no client connected for a period the operator sets. It is none of those.
A module may legitimately need an AMQP broker as a **backing service**, the way it needs a
database, and the provider that answers that is an ordinary module like any other: no seat, not
foundation, never raised at genesis, installed when something wants it and absent from a mesh
that does not. There is no retirement condition, because the day its last client disappears is
not a day anything is waiting for.
What is deprecated is AMQP as **the mesh's transport**, which is this whole document. The rule
that remains is about direction rather than software: *inter-module communication goes over the
bus.* A module may hold a broker, a database or a cache for itself; it may not use one as a
channel to another module.
**And with one purpose, which it did not have when this was written.** Revision, second review **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 ([ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md) (superseding [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 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. 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 **There is one bus, and no module is handed a broker as a resource.** A module's messaging is
@@ -436,7 +450,7 @@ find what changed and why.
**Still open:** **Still open:**
- ~~Whether EVENTS should be one stream or one per emitting module.~~ **Closed** - ~~Whether EVENTS should be one stream or one per emitting module.~~ **Closed**
([ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md)): one stream, and not as a ([ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md) (superseding [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 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 runs, because a provisioner is itself a module that needs a bus account to start. Bootstrapping
decides it. decides it.
+1 -1
View File
@@ -9,7 +9,7 @@ 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/0119-amqp-is-a-provision-not-the-bus.md
- 02-DECISIONS/0118-a-module-declares-its-own-seats.md - 02-DECISIONS/0118-a-module-declares-its-own-seats.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
@@ -5,7 +5,7 @@ code: []
updated: 2026-09-26 updated: 2026-09-26
decisions: decisions:
- 02-DECISIONS/0118-a-module-declares-its-own-seats.md - 02-DECISIONS/0118-a-module-declares-its-own-seats.md
- 02-DECISIONS/0117-the-bus-is-the-only-broker.md - 02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md
- 02-DECISIONS/0106-the-bus-is-nats.md - 02-DECISIONS/0106-the-bus-is-nats.md
- 02-DECISIONS/0041-events-are-a-relationship.md - 02-DECISIONS/0041-events-are-a-relationship.md
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md - 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
@@ -317,7 +317,7 @@ it as an ordinary module once the registry exists.
So there are exactly two things the normal path cannot make, both at genesis, both ending the So there are exactly two things the normal path cannot make, both at genesis, both ending the
moment the mesh can mint for itself: **the bus's own accounts** (§the bootstrap argument in moment the mesh can mint for itself: **the bus's own accounts** (§the bootstrap argument in
[ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md) — a provisioner is a module and [ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md)) — a provisioner is a module and
needs an account before it can run) and **the vault's own credential**. Any third exception is a needs an account before it can run) and **the vault's own credential**. Any third exception is a
design failure, and naming these two is what makes a third one visible. design failure, and naming these two is what makes a third one visible.
+1 -1
View File
@@ -39,7 +39,7 @@ document is written and this one's status becomes `implemented`.
| [`27-a-module-requires-the-mesh-resolves.md`](27-a-module-requires-the-mesh-resolves.md) | **Proposed.** One concept for everything a module needs: a requirement with a contract, answered by one of four kinds of provider, resolved at assignment or refused. Retires settings, placeholders, facts and paths in definitions | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md), [ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)) | | [`27-a-module-requires-the-mesh-resolves.md`](27-a-module-requires-the-mesh-resolves.md) | **Proposed.** One concept for everything a module needs: a requirement with a contract, answered by one of four kinds of provider, resolved at assignment or refused. Retires settings, placeholders, facts and paths in definitions | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md), [ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)) |
| [`28-building-the-bus.md`](28-building-the-bus.md) | **Proposed.** The five steps of the bus work in the order their dependencies allow, each ending at a bed — with the surface measured, so no step's size is a guess | [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md), [ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md), [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md) | | [`28-building-the-bus.md`](28-building-the-bus.md) | **Proposed.** The five steps of the bus work in the order their dependencies allow, each ending at a bed — with the surface measured, so no step's size is a guess | [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md), [ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md), [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md) |
| [`29-what-a-module-declares.md`](29-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md), [ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) | | [`29-what-a-module-declares.md`](29-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md), [ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md)), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) |
## Not yet written ## Not yet written