Files
hq/02-DECISIONS/0084-which-provider-serves-a-consumer.md
jschoubben fc4ab370d6 Graduate issues 067 and 068 to decisions and to-be designs
ADR 0084 (extends 0027) — a provision is served by a node-scoped provider the
consumer selects, defaulting to co-location; a module may instead carry a private
embedded instance that is not a provision. Design: 01-to-be/23-choosing-a-provider.

ADR 0085 (extends 0031) — a secret is a provision and the vault is the module that
provides it; a module's own local secret becomes an ordinary pair credential that
rotates through the existing machinery, while the controller's provisioning-credential
mint (0048) is unchanged. Design: 01-to-be/24-the-secrets-vault; doc 13 amended to
cross-link the non-pair secret.

Issues 067/068 marked resolved with amended-design set. ADR index regenerated;
records and index checks pass.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-20 21:27:29 +02:00

6.6 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
what runs on it accepted 2026-09-20 jochen false 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md

84. Which provider serves a consumer, when the mesh runs more than one

Context

ADR 0027 settled what a provision is named for — the thing the consumer's code is coupled to, so postgres-database and mssql-database are different provisions and a wrong match is refused at resolution. It said one thing more, in passing, and left it: "Two providers of postgres-database — a container on this node and a managed instance elsewhere — are interchangeable and should both match." Naming was decided; which of several providers serves a given consumer was not.

The mesh assumes there is only one to choose. Provisions are mesh-scoped: the adopted store is a single mesh-store a consumer on any node reaches, and scope: "mesh" is written into the provision definitions. That assumption is false on day one, and was always meant to be. Node-specific services delivered to the mesh is the plan, not an edge case:

  • Both control-capable nodes already run their own general-purpose relational store (the same engine, one instance each), serving that node's own applications.
  • Each runs its own SQL server, its own cache, its own object store. One node alone runs six separate relational-store instances, each raised by the module that needed it.
  • The one provision that is currently single — the identity provider, one instance on one node — already authenticates applications whose home is a different node.

So several providers of one provision name genuinely coexist, and they are not interchangeable the way 0027's aside supposed. They differ by node, by the data they hold, and by locality. A consumer bound to the wrong one reads the wrong database, or takes a cross-node hop it did not need, or cannot be moved without silently rebinding. The model has no field in which to say which one. This is 0027's own fault — a match that resolves and is wrong — one level up: 0027 refused the wrong dialect; nothing refuses, or even asks about, the wrong instance.

Considered Options

  1. Keep scope: "mesh" — one provider per provision, mesh-wide. Rejected: it is false on day one, and making it true would force every node's applications onto one node's server — the exact opposite of node-specific services delivered to the mesh, and a single point of failure the topology was built to avoid.
  2. Resolve to any provider of the name (0027's "both match"). Rejected: when providers hold different data and live on different nodes they are not interchangeable, and picking one arbitrarily is a wrong-instance match — the confidently-wrong answer 0027 exists to prevent, restated at the level of the instance rather than the dialect.
  3. Always require the consumer to name the provider explicitly. Rejected: needless ceremony in the common case, where the consumer wants the provider on its own node; and a field every manifest must carry is a field an author forgets, which then matches everything again — the failure 0027 warned about for qualifiers.
  4. A provision is node-scoped; the consumer selects the provider, defaulting to co-location. Adopted.

Decision

A provision is served by a provider identified by its node, and the consumer selects which one. A provider is a (node, module) pair, not a mesh-wide singleton. A consumer's binding resolves to a specific provider, and the selection is part of the assignment (ADR 0046), not the manifest.

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 common case and needs nothing said. A mesh with one provider of a kind is just the case where co-location and "the only one" coincide — expressed as there happens to be one, not as a scope.

A consumer coupled to a provider's data names it. Where two consumers must share one database, or a consumer must reach a provider on another node, the assignment names that provider — because that coupling is exactly what may not be guessed, and naming it is what makes a later move safe.

A module need not consume a provider at all. It may carry its own instance inside its own composition — on its own module network, publishing no host port, not declared as a provision — when a genuine engine fork or a pinned server version makes the shared provider unusable. Such an instance is invisible to resolution and can be bound by nothing else. The rule is share by default; embed only when a fork or a version forces it — most of the per-module stores that exist today are vanilla engines on stale pins that a consolidation onto the node's provider would absorb.

Consequences

The mesh can carry its real topology deliberately rather than by the accident of which provider happened to be the single one. A consumer's data-coupling becomes a stated fact, which is what lets a provider be moved without a consumer silently following the wrong one — provided a provider keeps its identity across a relocation, which is a follow-up this record opens rather than closes. Rotation (13) addresses a specific provider's holders, not "the provision's".

What got harder: an assignment now may carry a provider selection, and a wrong one is a new way to misconfigure. It is mitigated the way 0027 mitigated its own: the co-location default removes the choice in the common case, and genuine ambiguity — several providers, none named, none co-located — is refused with the candidates named, never resolved by picking.

References

  • ADR 0027 — the naming this extends; its "both match" aside is the gap closed here.
  • ADR 0031 — identity is exactly such a provider, and already serves consumers on another node.
  • ADR 0046 — where the selection lives.
  • ADR 0078 — the adopted store, whose mesh-wide binding is the single-provider assumption this record replaces.
  • issue 067 — the gap, and the day-one evidence.
  • 03-DESIGN/01-to-be/23-choosing-a-provider.md — the design.