Files
hq/02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
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

17 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
what runs on it accepted 2026-09-25 jochen false 0009-modules-and-the-graph.md

110. A seat is held by one assignment, from a closed set, and it may deliver a provision

Narrowed, not replaced — 2026-09-27, on merging two lines of work. This was marked superseded by ADR 0126, and that overstated it: 0126 says in as many words that "everything 0110 decided about what a seat is stands untouched". What moved is where the set lives and who may add to it — 0126 lets a module declare one and makes the set derived, 0121 names the mesh's own for their scope, and 0122 moves them out of code into a table. What a seat is — one holder at its scope, a definition saying what a module can hold against an assignment saying what it does, a role made singular rather than a module — is this record and still current, which is why those three rest on it.

Context

ADR 0009 introduced claims: a module declares something singular, at a scope, and a second holder is refused. ADR 0079 named the foundation's three after their servers. That mechanism is enforced and works. What it means has drifted, and four things are now true of it that no record says.

Any well-formed name becomes a seat by being claimed. The controller's manifest check refuses a claim only for a malformed name or an unknown scope. Nothing says which seats a mesh has. The names in use were each invented by the module that claims them: the-showcase, the-build-machine, the-intrusion-prevention.

Nothing can say what a mesh has, or who fills it. There is no seat table and no command that lists seats. Holdings are assembled while planning, one node at a time, and discarded afterwards. The only way to answer "which seats does this mesh have, and which module holds each" is to read every manifest in two repositories, because the controller's own manifest lives in its own repository (ADR 0069), and then the controller's code, because one module it ships has its manifest composed there. While this record was being prepared, that enumeration was done by hand, and it missed both of the last two sources: eleven claims were reported where there are thirteen.

A claim in a definition makes a module singular, not a role. The store module's definition claims mesh-store, so every assignment of it claims the seat, and a second store module on any other node is refused. What is singular is the store the mesh itself uses, not postgres. Any module can run on any node whose capabilities match, which is a core principle of the module system, and a claim written into the definition breaks it for every module that claims anything.

Some seats are the mesh's one of something everyone consumes, and nothing uses that fact. A requirement for a mesh-scoped provision with more than one provider is refused until a person pins, per consumer node, which provider to use. ADR 0109 anticipates exactly that case, gitea and verdaccio both answering npm, and under today's resolution it would mean a pin on every machine that builds anything.

Considered Options

1. Leave seats as free-form exclusion, claimed in definitions. Rejected. The overview stays unanswerable, a module that claims a seat can run on only one node, and a second provider of anything costs a pin per consumer node.

2. Two concepts: seats for exclusion, and a new word for the mesh's one consumable thing. Rejected. Both mean "this mesh's one X". Every existing claim would first have to be classified into one or the other, and the overview a person wants is one list, not two.

3. A seat is held by one assignment, from a closed set, and holding it may deliver a provision. Chosen.

Decision

The mesh defines a closed set of seats. Each entry has a name, a scope, what holding it delivers (if anything), and the decision that made it a seat. A seat outside the set is refused wherever it is named. Adding a seat is a decision, for the same reason adding a shape to the host's vocabulary is one: the set is what a person reads to learn what a mesh can have, and a name added without an argument is a name nobody can explain later.

A definition says which seats a module can hold. An assignment says which it does hold. The store module can hold mesh-store, and it may be assigned to every node. Exactly one of those assignments holds the seat, because that assignment said so. A second assignment saying so, at the seat's scope, is refused. So a seat makes a role singular, never a module, and moving the role is changing which assignment holds it, with no definition changed and nothing unassigned.

What the mesh knows about a seat's holder is what it knows about that assignment: its node, the node's settings for it, and what it serves. Holdings are not stored separately. The seat points at an assignment, and a second record of the same fact would be a second thing to disagree with the first.

Holding a seat may deliver a provision. A seat that delivers a provision may only be held by an assignment of a module that provides it, at the seat's scope.

A requirement may name a seat, and then the seat's holder answers it. Naming the seat asks for the mesh's one, not for whichever provider is nearest, so the holder answers even when another provider runs on the consumer's own node, and nothing is asked of anyone. Unheld, the requirement is refused, naming the seat. A builder asks for the mesh's npm registry this way, and is served by the holder of npm-package-registry wherever it runs, with no pin on any machine.

A requirement that names no seat resolves as ADR 0084 has it: a pin, then the provider on the consumer's own node, then the only provider. Where several remain and none is local, a person chooses when the module is assigned. Assignment lists the candidates, with the holder of a seat that delivers the provision suggested first, and records the answer on the assignment as its pin. Without an answer the module is not assigned. This keeps ADR 0009's rule that a requirement with several answers is never guessed. The choice is made either by the requirement naming the seat, or by a person at assignment, and never silently by what happens to run nearby. That is the failure issue 106 names for the vault.

A seat delivers a provision only where the mesh has one answer for everyone. The artifact store, the npm registry, git and the vault are each one per mesh by their own records, so their seats deliver them.

The foundation's seats deliver nothing. mesh-controller, mesh-store and mesh-broker name which assignment the mesh itself uses: the controller, the store holding its records, the broker carrying its bus. The store and broker modules may run on other nodes too, and a database or amqp consumer that names no seat is served by co-location from whichever runs on its own node, the seat's holder included. Were mesh-store to deliver, a consumer could name it and be sent to the store the mesh keeps its own records in. That is not a store for consumers.

A seat may reserve its provision. Where a second provider would break the reason the provision exists, only an assignment holding the seat may provide it at all: the parser refuses a definition that provides it without being able to hold the seat, resolution refuses an assignment providing it without holding the seat, and a pin cannot choose anyone else: a requirement for it always names the seat. secret is the one reserved provision. The vault is one per mesh because a second "would be a second place to lose" (ADR 0085, as amended), and a second secret provider is exactly that, whether a pin chose it or not.

Seats are also informational. The controller lists every seat in the set, what it delivers, and which assignment holds it, including seats nobody holds. An unheld seat is an answer, "this mesh has no X", not an error.

The first set is the twelve seats already claimed, plus two. Thirteen claims are in use, and they name twelve seats because two alternative modules claim the-resolver-configuration. This record admits every seat the catalogue and the controller claim today, so no definition is refused by it:

seat scope delivers can be held by made a seat by
mesh-controller mesh — mesh-controller 0079
mesh-store mesh — postgres 0079
mesh-broker mesh — lavinmq 0079
mesh-vault mesh secret, reserved mesh-vault this record, for issue 106
the-artifact-store mesh artifact-store distribution 0075
the-catalogue mesh — mesh-catalog this record
npm-package-registry mesh npm-package-registry gitea 0109
the-build-machine node — builder this record
the-dns-port node — dnsmasq this record
the-intrusion-prevention node — fail2ban this record
the-packet-filter node — nftables this record
the-private-network node — the controller's private-network module this record
the-resolver-configuration node — resolv-conf or resolved-split-dns this record
the-showcase node — showcase this record

There are two additions. npm-package-registry is ADR 0109's seat, named after the provision it delivers, as 0079 named the foundation's seats after what they are. A gitea assignment holds it. verdaccio provides the same provision and cannot hold the seat, so it is the second provider this record exists to make harmless. Moving npm to it would take a definition saying it can hold the seat, and then an assignment saying it does.

mesh-vault answers issue 106. The vault is one per mesh (ADR 0085, as amended), and until now that was enforced by nothing. The seat is named after its server, by the 0079 convention.

the-dns-port is listed as delivering nothing, although dnsmasq provides wildcard-resolution. That provision is node-scoped and answered on the machine, so no preference between providers arises.

What this changes in earlier records

On acceptance, each of these is amended by this record, not edited:

  • ADR 0009: a claim in a definition says a module can hold a seat; the assignment says it does.
  • ADR 0079 and ADR 0078: "a mesh runs one postgres and one lavinmq" becomes one holder of mesh-store and one of mesh-broker. The store and broker modules may run on other nodes.
  • ADR 0084: a requirement may name a seat, which its holder answers; and where several providers remain and none is local, the choice is asked when the module is assigned and recorded as a pin, rather than refused until someone pins it.
  • ADR 0109: one provision per package ecosystem stands. Where 0109 says seat, it means that provision. Only npm-package-registry is also a seat in this set. A cargo or docker registry becomes one by a record, as any seat does. "Gitea may hold several seats" reads: gitea may provide several ecosystems, and hold the seat of each one that is a seat. Moving npm to verdaccio is not "assigning npm-package-registry to verdaccio". It takes verdaccio's definition saying it can hold the seat, and then an assignment holding it.
  • To-be 23: the same two changes, in the design that describes choosing a provider.
  • To-be 21: genesis assigns the foundation's store, broker and controller holding their seats, where their definitions claim them today.

Consequences

  • The controller carries the set in code. A test asserts its size, and that every entry names the record that made it a seat, so changing the set means finding the argument rather than a number.
  • An assignment gains the seats it holds. Genesis assigns the foundation's store, broker and controller holding their seats, where today their definitions claim them.
  • Manifest validation refuses an unknown seat, a seat named at the wrong scope, and a delivering seat named by a module that does not provide the provision. Resolution refuses a second holder, and an assignment holding a seat its module cannot hold.
  • Resolution answers a requirement naming a seat with its holder. Assignment asks a person where several providers remain, suggesting the seat's holder first, and records the answer as a pin. A provider record gains the module it came from.
  • A seats command lists the set with each seat's holder, derived from assignments.
  • What got harder: a module wanting a new singular role can no longer invent a name. It needs a record. And an assignment has one more thing to say. Both are the point.
  • Not changed: the controller's seat placeholder stays as it is. It exists so the controller can reach a foundation it made before any module existed.

How it is checked

Rule Checked by
The set is closed, and every entry names its decision A controller unit test asserts the set's size and a non-empty decision for every entry.
A seat outside the set is refused Manifest-validation tests for an unknown seat, the wrong scope, and a delivering seat named by a module that does not provide it.
Every module in use names a seat in the set A controller test parses every catalogue manifest and fails on any refused seat. The lab's beds read the same manifests (ADR 0089).
A seat is held by one assignment, not by a module A resolution test: the store module assigned to two nodes resolves, with one assignment holding mesh-store; a second assignment asking to hold it is refused.
A requirement naming a seat is answered by its holder Resolution tests: two providers with the seat held; a second provider on the consumer's own node, where the holder still answers; the seat unheld with two providers, and with one provider, both refused naming the seat.
An assignment holds only a seat its module can hold A resolution test: an assignment holding a seat its definition does not name is refused.
Every seat is listed with its holder A seats command test: every seat in the set is listed with its scope, what it delivers and its holder, and an unheld seat is listed as unheld.
Several providers and none local is a person's choice An assignment test: the candidates are listed with the delivering seat's holder first; the answer is recorded as a pin; with no answer the module is not assigned.
The foundation's seats route nobody A resolution test: with the store module on two nodes, a database consumer is served by the one on its own node, whichever holds mesh-store; a requirement naming mesh-store is refused, because it delivers nothing.
A reserved provision has no other provider The parser refuses a definition providing secret that cannot hold mesh-vault; resolution refuses an assignment providing it without holding the seat, and a pin on a secret requirement.

References

  • ADR 0009: claims, scopes, and "refused, never guessed"
  • ADR 0079: a seat named after what it is
  • ADR 0109: the per-ecosystem registry seats
  • ADR 0084, to-be 23: pins, co-location and refusal
  • mesh-controller internal/catalogue/resolve.go (checkClaims, the brokered branch of Resolve), internal/catalogue/manifest.go (claim validation), cmd/mesh-controller/plan.go (holdings)