Files
hq/02-DECISIONS/0106-the-bus-is-nats.md
T
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

108 lines
6.3 KiB
Markdown

---
topic: the mesh
status: accepted
date: 2026-09-23
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0002-nodes-communicate-over-a-broker.md
---
# 106. The bus is NATS
## Context
The mesh's bus is an AMQP broker. [Research 014](../01-RESEARCH/014-the-bus-on-nats/00-overview.md)
measured what that costs and what it would take to change: AMQP is spoken in three places of the
mesh's own code — the controller's link, the host's link, the tool runtime's client — and in none of
the sdk or the modules, because [ADR 0039](0039-what-the-sdk-holds-and-refuses.md) kept the client
out of the sdk for exactly this. The predecessor's world is AMQP and is being retired module by
module; it cannot move and does not need to.
The operator's direction: NATS, native — the mesh built on it, not bridged to it — and the tools a
person reaches from a workstation designed on it rather than built quickly on what exists.
## Considered Options
1. **Stay on AMQP.** Rejected by the operator.
2. **NATS behind a bridge**, the mesh unchanged. Rejected: two buses, every guarantee crossing a
seam, and the reason for changing — one native, simple, subject-addressed bus with request/reply
and accounts built in — lost at the seam.
3. **NATS native, built in the lab in parallel with the migration, cut over in one rehearsed
rollout after the migration's core is done.** Adopted.
## Decision
**The mesh's bus is NATS.** Every link the mesh has — control, node queues, builds, enrolment,
reports, events, tool invocation — is carried on NATS subjects; durability, catch-up and
hold-unacknowledged-and-retry ([ADR 0083](0083-one-push-leaves-the-mesh-consistent.md)) are
JetStream's; a module's account is a NATS account with per-subject permissions derived from its
`emits` and `consumes` ([ADR 0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md)),
written as configuration the host declares and the server reloads — never through a management
API. The broker module changes; the seat `mesh-broker` does not
([ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md)).
**The sdk's broker contract does not change.** Modules are written against `request`, `handle`,
`publish`, `subscribe`; the runtime implements them on NATS; no module speaks a protocol.
**The AMQP broker the mesh adopted becomes the predecessor's compatibility broker**, kept as a
module with one purpose — the predecessor's clients — and retired with the last of them.
**It is built beside the migration and cut over after its core.** The bus is not changed under a
half-migrated node. A lab bed proves the whole path — enrolment, the store window, upgrades, tool
invocation — on NATS before any node's bus moves, and the move is one rollout: controller, every
host, every runtime together.
**What a person reaches the mesh's tools with is designed on NATS**, as part of the same design: a
person's account, scoped like a module's, and a client that speaks the bus directly.
## Consequences
- The architecture — subjects, streams, accounts, the enrolment handshake, the person's client — is
written as a to-be design before code, and reviewed.
- Eight records are touched: 0002 and 0033 (the substrate names a broker — now NATS), 0041 and
0042 (events keep their shape; the exchange becomes a subject prefix), 0043 (accounts as
configuration), 0078 (the broker module is `nats`), 0083 (JetStream carries the guarantee), 0039
(unchanged, and the reason this is possible).
- The lab beds that prove the bus are re-run on NATS; none is skipped.
- Until the cutover, nothing changes on any node.
## How it is checked
A lab bed raises a mesh on NATS from genesis and proves: a node enrols over TLS with a claimed
token; a push composes, is held while the store restarts and applies after; an upgrade rolls out;
a module's tools are invoked from another node and from a person's client; an event dead-letters
after its deliveries are exhausted; a module's account cannot publish outside its `emits` nor
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.
## 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 0125](0125-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.
> **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 0125](0125-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 0127](0127-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
- [research 014](../01-RESEARCH/014-the-bus-on-nats/00-overview.md)
- [ADR 0002](0002-nodes-communicate-over-a-broker.md), [0033](0033-the-substrate-is-a-store-and-a-broker.md),
[0039](0039-what-the-sdk-holds-and-refuses.md), [0043](0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md),
[0078](0078-the-store-and-broker-are-modules.md), [0083](0083-one-push-leaves-the-mesh-consistent.md)