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

221 lines
16 KiB
Markdown

---
topic: what runs on it
status: superseded
superseded-by: 02-DECISIONS/0118-a-module-declares-its-own-seats.md
date: 2026-09-25
deciders: jochen
reconstructed: false
extends: 0009-modules-and-the-graph.md
---
# 110. A seat is held by one assignment, from a closed set, and it may deliver a provision
## Context
[ADR 0009](0009-modules-and-the-graph.md) introduced claims: a module declares something
singular, at a scope, and a second holder is refused. [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md)
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](0069-a-module-is-a-repository-and-a-path.md)), 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](0109-a-package-registry-seat-is-one-per-ecosystem.md)
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](0084-which-provider-serves-a-consumer.md)
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](0009-modules-and-the-graph.md)'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](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md) 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](0085-a-secret-is-a-provision.md), 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](0079-the-foundation-seats-are-named-after-their-servers.md) |
| `mesh-store` | mesh | — | `postgres` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
| `mesh-broker` | mesh | — | `lavinmq` | [0079](0079-the-foundation-seats-are-named-after-their-servers.md) |
| `mesh-vault` | mesh | `secret`, reserved | `mesh-vault` | this record, for [issue 106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md) |
| `the-artifact-store` | mesh | `artifact-store` | `distribution` | [0075](0075-two-stores-and-which-provides-what.md) |
| `the-catalogue` | mesh | — | `mesh-catalog` | this record |
| `npm-package-registry` | mesh | `npm-package-registry` | `gitea` | [0109](0109-a-package-registry-seat-is-one-per-ecosystem.md) |
| `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](0109-a-package-registry-seat-is-one-per-ecosystem.md)'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](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md). The vault is one
per mesh ([ADR 0085](0085-a-secret-is-a-provision.md), 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](0009-modules-and-the-graph.md): a claim in a definition says a module *can* hold a seat;
the assignment says it does.
- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md) and
[ADR 0078](0078-the-store-and-broker-are-modules.md): "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](0084-which-provider-serves-a-consumer.md): 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](0109-a-package-registry-seat-is-one-per-ecosystem.md): 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](../03-DESIGN/01-to-be/23-choosing-a-provider.md): the same two changes, in the design
that describes choosing a provider.
- [To-be 21](../03-DESIGN/01-to-be/21-the-installation-in-full.md): 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](0089-a-bed-reads-the-catalogue-it-proves.md)). |
| 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](0009-modules-and-the-graph.md): claims, scopes, and "refused, never guessed"
- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md): a seat named after what it is
- [ADR 0109](0109-a-package-registry-seat-is-one-per-ecosystem.md): the per-ecosystem registry seats
- [ADR 0084](0084-which-provider-serves-a-consumer.md), [to-be 23](../03-DESIGN/01-to-be/23-choosing-a-provider.md):
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)