Two decisions taken with the author: - Genesis delivers and the vault adopts. The vault cannot run first — it is built on the runtime base the installation makes after the store, broker and controller, and it learns its work over the bus. Genesis generates the foundation's first shared secrets, seals them to the operator key, and delivers them to the vault through the path an operator's value takes; from then on the vault holds and rotates them. This answers ADR 0085's own reason for rejecting vault-only minting, which 0113 now names instead of stepping around. - Rotation re-confirms on every pass. An applier repeats its confirmation until acknowledged, so a lost message costs one pass; an applier that stops after applying locks readers out until its supervised restart, and that window is stated and shown, not claimed away. Fixes: - Scope: a shared secret is made by the vault; a private key (node sealing keys, the operator's key, the certificate authority) is made where it is used. The inventory adds the makers the first version missed: node and builder broker passwords, and enrolment tokens. - Broker accounts are created by the broker's provisioner, not the controller, so the controller never holds their plaintext; mesh-broker delivers amqp again — one broker per mesh — and only mesh-store delivers nothing. - secret is a reserved provision: only the mesh-vault holder may provide it, and no pin routes around it. - A secret's contract says whether a recipient applies it or reads it at start; appliers are never restarted for it, init-only secrets are applied, and confirmation is to-be 13's standard. - Operator secrets are one rule everywhere: a secret requirement answered by the vault (0112 no longer says otherwise). A data provider's adapter may return fields; the data-return check names a lab consumer. - 'Holder' now means a seat's holder only; a secret has recipients.
100 lines
6.2 KiB
Markdown
100 lines
6.2 KiB
Markdown
---
|
|
layer: to-be
|
|
status: designed
|
|
code: []
|
|
updated: 2026-09-25
|
|
decisions:
|
|
- 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](../../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.
|
|
|
|
**Some provisions have one provider for the whole mesh, and a seat names it.** Where a seat delivers
|
|
the provision, its holder answers for it, **and co-location does not apply**: a second provider on the
|
|
consumer's own machine does not take over for that consumer. 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](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md),
|
|
[26 — The seats](26-the-seats.md)). A named provider still wins over the seat, because a consumer
|
|
coupled to particular contents has said so, except for `secret`, which only the vault may provide.
|
|
Only provisions the design makes one-per-mesh are delivered by a seat: the broker, the artifact store,
|
|
a package registry, git and the vault. A database is not. Node-local stores, served by co-location,
|
|
are the rule above.
|
|
|
|
**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](../../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.
|