Files
hq/03-DESIGN/01-to-be/23-choosing-a-provider.md
T
jochen dbe100ca96 ADR 0110 and 0111: a seat is a module assignment from a closed set, and a build source may live on the git seat
Seats have been doing two jobs and neither is written down. The mechanism ADR 0009 introduced is
enforced — a second holder is refused — but any well-formed name becomes a seat by being claimed,
and nothing can say which seats a mesh has or who holds them: holdings are assembled while planning
and discarded. The enumeration done while preparing this missed the control plane's own manifest,
because core modules' manifests live in its repository rather than the catalogue.

0110 closes the set. Each seat has a name, a scope, what occupying it delivers, and the record that
made it one; a claim outside the set is refused. A seat is held by a module assignment, and what the
mesh knows about the holder is what it knows about that assignment — nothing is stored beside it. A
seat may deliver a provision, and then its holder answers for it among several providers: pin, then
the holder, then the only provider, then refused. That keeps 0009's "refused, never guessed": the
seat is the choice made once, mesh-wide, instead of a pin per consumer node. The first set is the
eleven seats already claimed plus 0109's npm-package-registry, so nothing in use is refused.

Two concepts — seats for exclusion, a new word for consumable singulars — was rejected: both mean
"this mesh's one X", and the overview a person wants is one list.

0111 gives the mesh a git seat and makes a build source one of two explicit forms: a repository on
the seat's holder, recorded by its path and cloned from wherever the holder runs at build time; or
an external URL, recorded and cloned exactly as given. Recognising self-hosted sources by matching
URLs against the forge's address was rejected — it fails in the one case it exists for, after the
forge moves. Credentials for private repositories are left undecided and said so.

Design: new to-be 26 (the seats); 23 gains the seat step in resolution; 18's source entry names
the two forms; the glossary's seat and provision entries say where they meet. 0109 is carried from
its own branch so every link here resolves.
2026-09-25 20:33:14 +02:00

5.8 KiB

layer, status, code, updated, decisions
layer status code updated decisions
to-be designed
2026-09-25
02-DECISIONS/0084-which-provider-serves-a-consumer.md
02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md

23 — Choosing a provider

A provision is named for what the consumer's code is coupled to (ADR 0027): postgres-database, not database. That decides what kind of provider satisfies a requirement. It does not decide which provider, and the mesh runs more than one of most kinds.

Why there is a choice at all

Node-specific services delivered to the mesh is the design, not an exception. Every control-capable node runs its own relational store, its own cache, its own object store; a single node may run several relational stores, each raised by the module that needed a particular engine or version. The one provision that is single today — the identity provider — already serves applications whose home is another node. So for a given provision name there are usually several providers, one per node, and they are not interchangeable: each holds different data and lives in a different place. A consumer bound to the wrong one reads the wrong database or takes a network hop it did not need.

Naming the kind is therefore only half of "how a consumer gets what it needs". The other half is which provider, and it has two shapes: consume a provider, or carry your own.

Consuming a provider

A provider is not a mesh-wide singleton. It is identified by the node it runs on together with the module that provides it — a (node, module) pair. A consumer's requirement resolves to one such provider, and which one is part of the assignment, not the manifest (ADR 0046): the same module, assigned twice, may be served by two different providers.

The default is co-location. A consumer that names no provider is served by the provider of that provision on its own node. This is the ordinary case and is meant to need nothing said — a module that wants a database wants, almost always, the database on the machine it runs on. A mesh that happens to run exactly one provider of a kind is simply the case where co-location and "the only one there is" name the same thing; that is there happens to be one, not a mesh-wide scope written into the provision.

Coupling to data is named. The exception to co-location is a consumer coupled to a particular provider's contents: two modules that must share one database, or a consumer that must reach a provider on a different node. That coupling is exactly what may not be guessed, so the assignment names the provider. Naming it is also what makes a later move safe — the mesh knows the binding is to that provider and not to whichever one is nearest.

A seat names the mesh's one provider of a kind. Where a seat delivers the provision, its holder answers for it when several providers exist and the consumer named none. That is not picking: the choice was made once, mesh-wide, by assigning the holder, rather than once per consumer by naming it (ADR 0110, 26 — The seats). A named provider still wins over the seat, because a consumer coupled to particular contents has said so.

Ambiguity is refused, never resolved by picking. If several providers of a kind exist, none is named, none is co-located, and no seat delivers it, the requirement is unsatisfiable and is refused with the candidates shown — the same stance ADR 0027 took against a confidently-wrong match, applied to the instance rather than the dialect. A wrong answer delivered quietly costs more than a refusal.

Carrying your own

A module need not consume a provider at all. It may carry its own instance of an engine inside its own composition — reachable only on the module's own network, publishing no host port, and not declared as a provision. Nothing else in the mesh can see it or bind to it, and it cannot collide with anything on a well-known port. To resolution it does not exist; it is an internal part of the module, like any other container the module runs.

This is legitimate but it is the exception, and the design says when: only when a genuine engine fork or a pinned server version makes the shared provider unusable. A module written against a customised engine, or one that needs an extension the node's provider does not carry, has no choice but to carry its own. A module that merely pins an old image of an ordinary engine does not — the version on a compose file is the server's, and the application talks to a newer shared server perfectly well once its data is migrated in. The rule is share by default; embed only when a fork or a version forces it. Most of the per-module stores that exist in the mesh being migrated onto are the first kind wearing the second's clothes, and consolidate onto the node's provider.

The distinction is worth stating because the two cases look identical from outside — a module with a database either way — and the mesh must be able to tell them apart to reason about either. A consumed provider is a binding the mesh records, rotates and can move. An embedded instance is a private detail the mesh does not manage and must not mistake for a provider.

What is not settled here

A provider that moves between nodes must keep its identity, so that a consumer's recorded choice does not silently rebind to a different provider that inherited its place. That is a property the provider lifecycle must supply, and this document names it as a requirement rather than describing its mechanism.