Files
hq/02-DECISIONS/0118-a-module-declares-its-own-seats.md
T
jschoubben 7b4916e9ec Modules declare their own seats; the mesh reserves mesh-*
The architecture 0117 opened needs a module to offer a service as a role on
the bus — one holder, addressed by what it does. A closed table in the
controller cannot express that: a capability a module contributes would
require changing the mesh itself.

But 0110 closed the set for a good reason — nothing could say what seats a
mesh had, and the hand count came out at eleven of thirteen. That argues for
enumerable, not hardcoded, and 0110 weighed free-form against a fixed table
without considering a third option: closed at any moment and derived from
the catalogue. A derived list cannot drift, which is how the count broke.

So: the mesh's seats stay the mesh's, reserved by the mesh- prefix so the
prefix is the rule and there is no list to maintain; ten seats are renamed
to restore 0079's convention; everything 0110 decided about what a seat IS
survives untouched.

Design 29 carries the declaration model: three namespaces, subjects derived
from local names so a manifest survives the wire changing, queues never
declared, five relationships (the job and state shapes 0041 had no room
for), and the build-publish-deploy lifecycle with hard, soft and build-time
dependencies distinguished.

0041 gets a progressive insight: "no per-consumer setup, only a
subscription" was a fact about a topic exchange, and a JetStream durable
consumer is a real object someone creates.

WBS 1.3/1.4 were wrong and say so: streams come at registration and
consumers at assignment, so only the foundation set belongs at genesis.
2026-09-26 20:34:32 +02:00

131 lines
8.1 KiB
Markdown

---
topic: the tiers
status: accepted
date: 2026-09-26
deciders: jochen
reconstructed: false
supersedes: 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
---
# 118. A module declares its own seats; the mesh reserves its own
## Context
[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) closed the set of seats. Its
evidence was strong and still is: nothing could answer *which seats does this mesh have, and who
holds each*. Answering it meant reading every manifest in two repositories and then the
controller's own code, and when that enumeration was done by hand while writing the record, **it
reported eleven claims where there were thirteen.** The fix was a table in the controller, and
adding a seat became a decision.
What that table cannot express is the architecture [ADR 0117](0117-the-bus-is-the-only-broker.md)
opened. With one bus and no private brokers, a module offering a service to other modules offers
it as **a role on the bus**: a set of subjects, exactly one holder, addressed by what it does
rather than by which module or node provides it. A telegram sender, a licensing master, anything
a mesh might want one of. Under a closed table, adding any of those means editing the controller
— so a capability contributed by a module would require a change to the mesh itself, which is the
coupling the module system exists to prevent.
**The two requirements look opposed and are not.** 0110 needs the set *enumerable*. The
architecture needs it *extensible*. Those conflict only if enumerable means *written down in one
place by hand* — which is exactly the property that let the count drift in the first place.
## Considered Options
1. **Keep the closed table, add each new seat by decision.** Rejected. Every capability a module
contributes would need a change to the controller and a record before it could be offered, and
the mesh would carry the names of services it does not itself implement.
2. **Free-form seats, as before 0110.** Rejected for 0110's own reason, unchanged: nothing can
say what a mesh has, and a name invented at a claim site is a name nobody can explain later.
3. **A set that is closed at any moment and derived rather than maintained**, with the mesh's own
seats reserved by name. Adopted. 0110 weighed options 1 and 2 and never considered this one.
## Decision
**A seat may be declared by a module, and the set of seats a mesh has is derived: the mesh's own,
plus those declared by every module it has registered.** The set is still closed — a seat named
nowhere is refused — but it is computed from the catalogue rather than written in the controller.
Everything 0110 decided about what a seat *is* stands untouched: one holder at its scope; a
definition says which seats a module *can* hold and an assignment says which it *does*; holding
one may deliver a provision; a seat makes a role singular, never a module.
**Enumeration is a query, not an inventory.** The catalogue knows every registered manifest, so
"which seats does this mesh have, and who holds each" is answered by asking it. This is a
stronger answer than the table gave, not a weaker one: a derived list cannot drift from reality,
and drift is how the hand-made count came out at eleven of thirteen.
**The mesh's own seats are reserved by prefix.** Every seat the mesh itself defines is named
`mesh-*`, and a module declaring any `mesh-*` name is refused at registration. The prefix *is*
the reservation rule — no list of reserved names to maintain, and no way for the mesh's own
namespace to be colonised by a manifest. This requires renaming the seats that drifted from
[ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md)'s convention: `the-catalogue`
becomes `mesh-catalog`, `git` becomes `mesh-git`, and the node-scoped `the-build-machine`,
`the-dns-port`, `the-intrusion-prevention`, `the-packet-filter`, `the-private-network`,
`the-resolver-configuration`, `the-showcase` take the same prefix.
The mesh's seats stay the mesh's for a reason that does not apply to a module's: **the mesh's own
code looks them up by name.** The resolver *is* the thing that finds the store. `mesh-store` is
not a convention the controller follows, it is an identifier the controller dereferences.
**A declared seat carries a protocol.** A module declaring a seat says what may be sent to it,
what it emits, and what it serves. The holder must satisfy it; a module may not claim a seat whose
protocol it does not implement. Callers declare that they use the *seat*, never the module, so
replacing the implementation changes nothing for any caller.
**A seat is for a role; an event stays addressed to its emitter.** The two are not
interchangeable and the choice is not stylistic. An event is *this happened to me* — the emitter's
identity is the meaning, which is why the envelope carries source, node and time
([ADR 0042](0042-the-shape-of-an-event-on-the-wire.md)); routing it through a role would erase the
provenance an audit needs. A seat is *this capability, whoever provides it* — where not knowing
the holder is the point. Publish an event when the fact is about you; declare a seat when you are
offering something another module could offer instead.
**Two modules declaring the same seat name is refused at registration**, second one loses.
Registration is the last moment the mesh can still say no, and a seat name meaning two different
protocols is the failure nobody could diagnose afterwards.
## Consequences
- **The controller's seat table stops being the set** and becomes the mesh's own reserved entries.
Resolution reads the catalogue for the rest.
- **Ten seats are renamed.** A rename is a migration, not an edit: existing assignments hold the
old names, so the change carries a mapping and is applied once, and the lab beds that name seats
are updated with it.
- **A `uses` naming an undeclared seat is refused at registration**, which is where 0110's
guarantee lands under this model — the same refusal, at the same moment, from a derived set.
- **Adding a capability stops requiring a decision record.** That is a real loss of governance and
the intended trade: the argument for a seat's existence moves into the module that declares it,
where it is reviewed as part of the manifest. The mesh's own seats keep the old bar.
- **[ADR 0041](0041-events-are-a-relationship.md)'s machinery claim is already stale** for a
different reason, and is corrected in place there under the rule in
[`README.md`](README.md) — a progressive insight: on JetStream a subscription is a durable
consumer, a real object someone must create.
- **What got harder:** a seat's protocol is now a compatibility surface between modules that do
not know each other. Changing one breaks callers already bound to it, and nothing here says how
that is versioned. It is the first thing to answer in the design, and the thing most likely to
hurt later rather than now.
## How it is checked
- **The overview answers, and is right.** A command lists every seat, its scope, its protocol and
its holder, derived from the catalogue — and a test asserts the count against a fixture mesh,
because an enumeration nobody checks is how thirteen became eleven.
- **`mesh-*` is refused to a module.** A registration test: a manifest declaring `mesh-anything`
is refused, naming the prefix as the reason.
- **An undeclared seat is refused.** A registration test on `uses`, and a resolution test that
nothing reaches runtime unresolved.
- **A second declarer loses.** A registration test: two manifests, same seat name, the second
refused and the first untouched.
- **A holder must satisfy the protocol.** A claim whose module does not serve what the seat
declares is refused at assignment, not discovered when a caller times out.
## References
- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) — superseded here; its
requirement is kept and only its mechanism replaced.
- [ADR 0117](0117-the-bus-is-the-only-broker.md) — one bus, which is what makes a role addressable.
- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md) — the naming convention
the reserved prefix restores.
- [ADR 0041](0041-events-are-a-relationship.md) — the event half of the boundary drawn here.