Decided with the author: - A seat is held by one assignment, not claimed by a definition. A definition says which seats a module can hold; an assignment says which it does. The store module can run on every node and one assignment holds mesh-store; moving a role changes an assignment, never a definition. The foundation's seats name what the mesh itself uses and route no consumer — database and amqp consumers use co-location, the holder included. This replaces the wrong rationale that the foundation's store is "provider to nobody", which contradicted ADR 0078 and to-be 21. 0079's one-postgres rule becomes one mesh-store holder. - A module is assigned at most once to a node. The instance identity in 0112 and 27 is withdrawn, and the login-length problem with it. Review fixes to 0113: - The bottom of the stack: the vault is installed as soon as the shared runtime base exists, and genesis generates everything needed until then — including the permanent controller's, the control-node agent's, the builder's and the broker provisioner's bus accounts, and the controller's store login. Genesis creates those accounts until the broker's provisioner runs and adopts them. - Genesis's values are delivered recorded as the mesh's own, so 0092's never-replace rule for operator values does not make them unrotatable. - Backend-issued secrets (a forge's once-only API token) enter through the vault. Non-module parties (the controller's logins, node agents' accounts) are answered the same way, the controller asking on their behalf; an enrolment token reaches the controller only as what verifies it. - A secret with no provisioner to apply it is marked not rotatable by the mesh and refused, instead of a restart reported as done. Unused password generators in six provider clients are removed, and a catalogue scan checks no module mints. - Rotation's lock-out cases (offline reader, bus account owner, restarted provisioner) are recorded as open, with overlap and re-confirm-with-safeguards as the two answers, to be chosen before acceptance. 0110, 0111 and 26 are marked proposed: they changed in meaning and are under review, and an accepted record must not rest on proposed ones. To-be 23 and the glossary are restored to main; they change when these records are accepted.
88 lines
5.2 KiB
Markdown
88 lines
5.2 KiB
Markdown
---
|
|
layer: to-be
|
|
status: designed
|
|
code: []
|
|
updated: 2026-09-20
|
|
decisions:
|
|
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
|
|
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
|
|
---
|
|
|
|
# 23 — Choosing a provider
|
|
|
|
A provision is named for what the consumer's code is coupled to
|
|
([ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md)):
|
|
`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](../../02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md)):
|
|
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.
|
|
|
|
**Ambiguity is refused, never resolved by picking.** If several providers of a kind exist, none is
|
|
named, and none is co-located, the requirement is unsatisfiable and is refused with the candidates
|
|
shown — the same stance
|
|
[ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) 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.
|