Files
hq/03-DESIGN/01-to-be/26-the-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

12 KiB

layer, status, code, updated, decisions
layer status code updated decisions
to-be implemented
mesh-controller internal/catalogue/seats.go
mesh-controller internal/catalogue/resolve.go
mesh-controller cmd/mesh-controller/seats.go
mesh-controller cmd/mesh-controller/source.go
mesh-controller internal/inventory/migrations/0032-a-source-may-live-on-a-seat.sql
mesh-catalog modules/gitea/module.json
2026-09-26
02-DECISIONS/0118-a-module-declares-its-own-seats.md
02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md

26 — The seats

What a mesh can have one of, and who fills each. A seat is a named role at a scope, held by one module assignment. The mesh defines which seats exist. Holding one may deliver a provision, and the list of seats with their holders is the quickest answer to "what is in this mesh".

What a seat is

A seat has four properties, fixed by the mesh rather than by any module:

property is
name what a definition names and an assignment holds, and what a person reads in the list
scope node, site or mesh: where its capacity applies. Every seat in the set has a capacity of one, so one holder per scope. A bench, a seat with several holders, is a word the glossary keeps and no seat uses yet
delivers the provision its holder answers for, or nothing
decision the record that made it a seat

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 run on every node whose capabilities match. Exactly one of those assignments holds the seat, because that assignment says so, and a second assignment saying so is refused. A seat makes a role singular, never a module.

The seat points at the assignment. Everything the mesh knows about the holder is what it knows about that assignment: the node, the node's settings for the module, and what the module serves.

The set is closed. A seat the mesh does not define is refused wherever it is named, and so is one named at the wrong scope. Adding a seat is a decision, recorded, for the reason every addition to the host's vocabulary is one: the set is what a person reads to learn what a mesh can have, and an entry nobody argued for is an entry nobody can explain.

The set

The set is derived, and only the mesh's half is written here. Revision, 2026-09-26 (ADR 0118, superseding ADR 0110): a module declares its own seats with their protocols, so the seats a mesh has are the mesh's own plus every registered module's. The set is still closed — a seat named nowhere is refused — but it is computed from the catalogue rather than maintained by hand, which is the property 0110 actually needed and the table could not keep.

Every seat below is named mesh-*, and the prefix is the reservation rule: a module declaring any mesh-* name is refused at registration, so there is no reserved-names list to drift. Ten of these are renamed to restore ADR 0079's convention, which later seats departed from.

| seat | was | scope | delivers | typically held by | |---|---|---|---| | mesh-controller | — | mesh | — | the controller | | mesh-store | — | mesh | — | the store the mesh's own records live in | | mesh-broker | — | mesh | — | the broker carrying the mesh's own bus | | mesh-vault | — | mesh | secret, reserved | the vault | | mesh-artifact-store | the-artifact-store | mesh | artifact-store | the artifact registry | | mesh-catalog | the-catalogue | mesh | — | the catalogue | | mesh-npm-package-registry | npm-package-registry | mesh | npm-package-registry | the forge | | mesh-git | git | mesh | git | the forge | | mesh-build-machine | the-build-machine | node | — | a builder | | mesh-dns-port | the-dns-port | node | — | the local resolver | | mesh-intrusion-prevention | the-intrusion-prevention | node | — | an intrusion-prevention service | | mesh-packet-filter | the-packet-filter | node | — | the packet filter | | mesh-private-network | the-private-network | node | — | the private network the mesh runs over | | mesh-resolver-configuration | the-resolver-configuration | node | — | whichever of the alternative resolver configurations is chosen | | mesh-showcase | the-showcase | node | — | the showcase module |

The controller holds the mesh's own entries in code, and a test asserts their size and that every one names the record that made it a seat. A module's seats are not here and never will be — they are read from the catalogue. This table and ADR 0118 govern, and code that disagrees is what is wrong. The implementation in progress predates several things here: seats held by assignments rather than claimed by definitions, the mesh-vault seat and its reservation, and the foundation's seats delivering nothing. It is brought to this table before it merges.

The foundation's seats

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. They route no consumer. The store and broker modules may run on other nodes too. A database or amqp consumer is served by co-location, from whichever runs on its own node, the seat's holder included (23 — Choosing a provider). A requirement cannot name one of them, because they deliver nothing.

A seat that delivers a provision

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 decision. 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 the seat, and then its holder answers. Naming the seat asks for the mesh's one, so the holder answers even when another provider runs on the consumer's own machine, and nobody is asked anything. With the seat unheld, the requirement is refused, naming the seat. A second provider can run beside the holder and harm nothing. A forge assignment holds npm-package-registry, and an npm proxy may provide the same provision on another machine. A builder that names the seat is still served by the forge, without anybody pinning it.

A requirement that names no seat resolves as any other: a pin, the provider on the consumer's own machine, the only provider. If several remain and none is local, a person chooses when the module is assigned. The candidates are listed with the seat's holder suggested first, and the answer is recorded as the assignment's pin (27). Nothing is guessed, and nothing changes silently because a second provider happened to appear nearby.

Moving the role is changing which assignment holds the seat. No definition changes and nothing is unassigned: the forge keeps running, and keeps holding git, when its npm role moves. A module can take the role only if its definition says it can hold the seat.

The vault's provision is reserved. Only an assignment holding mesh-vault may provide secret at all: a definition providing it that cannot hold the seat is refused, an assignment providing it without holding the seat is refused, and a secret requirement always names the seat, because there is no other provider. A second provider of secrets would be a second place secrets live, which is what the vault being one per mesh exists to prevent.

What a consumer receives is what it required, the same as for any provision: where the provider answers, what it serves, and a credential. A consumer never reads the seat directly. The one exception is the controller itself, which reaches the store and the broker through a narrow seat placeholder, because it made them before any module existed and cannot be their consumer. One foundation module also reads it today, to find its own server's port. 27 moves that to a host port requirement.

A seat that delivers nothing

Most node seats deliver nothing. They say which module is this machine's packet filter, or which of two alternative resolver configurations it runs, and a second holder is refused. That is the whole of their job, and it is a real one: it is the mesh saying what a machine is, in words a person can read.

The overview

The controller lists every seat in the set with its scope, what it delivers, and its holder as a node and a module. A seat nobody holds is listed as unheld. That is an answer, "this mesh has no forge", and not a fault.

Holdings are derived from assignments whenever they are asked for, never stored. The list is always what the mesh is running, because it is computed from the same thing that decides what the mesh runs.

The git seat, and where a build comes from

A module is built from a repository, a path and a ref. The repository is one of two things, and the mesh records which:

form means recorded as
on the git seat a repository on the forge that holds the seat its path on the forge, and the seat
external a repository anywhere else, a public forge for instance its URL, exactly as given

For a repository on the seat, the controller composes the clone URL at the moment of building, from where the holder runs and the scheme and port it serves for git. The recorded source never contains an address, so moving the forge changes nothing that was recorded. The build machine is not told the difference: it receives a URL either way.

With the seat unheld, a build from the seat is refused and says why. External builds carry on.

Not yet designed: a credential for cloning a private repository. The mesh's own repositories are public. The natural place for a clone credential is a secret from the vault, and that is a decision still to take.

How it is checked

The rules here are ADR 0118's — which supersedes ADR 0110 and keeps every rule below except how the set is formed — and ADR 0111's. Each is checked as their tables say:

Rule Checked by
The set is closed, and every mesh entry names its decision 0118: a unit test on the mesh's own entries; manifest tests refusing an unknown seat or the wrong scope.
The set is derived, and enumerating it is a query 0118: the overview lists the mesh's own plus every registered module's, asserted against a fixture mesh.
mesh-* is the mesh's, and a module may not declare one 0118: a registration test refusing a manifest that declares any mesh-* seat, naming the prefix.
Two modules cannot declare the same seat 0118: a registration test; the second is refused and the first untouched.
A holder satisfies the seat's protocol 0118: a claim whose module does not serve what the seat declares is refused at assignment.
A seat is held by one assignment, and only by one whose module can hold it 0118: resolution tests for a second holder and for a seat the definition does not name.
A requirement naming a seat is answered by its holder; a foundation seat cannot be named 0118: resolution tests with a second provider on the consumer's node, with the seat unheld, and naming mesh-store.
Several providers and none local is a person's choice 0118: an assignment test listing candidates with the seat's holder first and recording the pin.
secret is reserved 0118: the parser and resolution refusals for another provider and a pin.
Holdings are derived, and the overview lists every seat 0118: the seats command test, including an unheld seat.
A build source on the seat records no address; an unheld seat refuses only self-hosted builds 0111's tests.