Naming which provider is only half of how a module gets a database. The other half: a module may carry its own version/fork-pinned instance, module-network only, no published port, not a provision — and the model has no word for it. Add it as a second axis with its own open questions. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
6.6 KiB
status, opened, located-in, fixed-by, amended-design
| status | opened | located-in | fixed-by | amended-design |
|---|---|---|---|---|
| open | 2026-09-20 |
A provision cannot name which provider serves it
Symptom, as observed
The mesh models every provision — postgres-database, s3-bucket, amqp, a
future oidc — as mesh-scoped: there is one provider of a given kind for the
whole mesh, and a consumer that requires the provision is bound to that one.
scope: "mesh" is written into the provision definitions, and the adopted store
is a single mesh-store.
The mesh being migrated onto is not shaped that way, and never was meant to be. Node-specific services delivered to the mesh was the plan from the start. Each node already runs its own provider of the same kinds:
- Both control-capable nodes run their own general-purpose postgres server (the same image, one per node), serving that node's own applications.
- Each node runs its own SQL server, its own redis, its own object store — infrastructure is per node, by design, not a single mesh-wide instance.
- Identity is the only provision that is currently single (one realm on one node), and even that already serves applications hosted on a second node.
So the multi-provider reality is not a future edge case that appears "the day a
second provider is added" — it is the founding topology, true today, on every
provision kind. What is missing is any way to say it. A consumer requires
postgres-database; it cannot require this node's postgres rather than that
node's. It requires oidc; it cannot name which node's identity provider. The
mesh model collapses a deliberately per-node fleet down to one mesh-scoped
provider, and a consumer has no field in which to choose.
Why it matters beyond this instance
- The model regressed an intended topology, it did not merely miss a corner
case. "Node-specific services delivered to the mesh" is the design;
scope: "mesh"with a singlemesh-storeexpresses the opposite. This is a gap between a stated intent and what the manifests can represent, which is exactly what the issues process is for. - It is not an identity special case. The missing concept — a provision has a provider, providers are per-node, and a consumer names the provider — is the same for databases, object stores, brokers and identity. A fix aimed only at SSO would leave the same wall standing behind postgres and minio, both of which are already multi-provider on the live mesh.
- The single-provider assumption is silent. Nothing rejects a second provider of a mesh-scoped provision; the model just cannot address it, so a consumer binds to whichever one is "the" provider — by accident of there being one, or by a race when there are two. A rule enforced by nothing ("there is one provider per provision") reads as true until the second node's provider makes it false, with no diagnostic at the seam.
- It blocks the migration concretely. The mesh already has applications on one node depending on another node's provider (identity today; databases the moment an app is assigned to a node whose local postgres is not "the" mesh store). Modelled as mesh-scoped, that topology is expressible only by accident. To carry it deliberately the provision must be able to name its provider.
A second axis: provisioned against a provider, or embedded and private
Naming which provider is only half of "how a module gets a database". There is a second, distinct case the model also cannot express: a module that does not consume a shared provider at all, but carries its own instance inside its own composition — on its own module network, publishing no host port, visible to nothing else in the mesh. This is legitimate and sometimes necessary: some containers pin a database server version or need a fork or extension set (a customised postgres, a vector extension) that the node's shared provider does not offer, so they must run their own alongside the main container.
The distinction that matters:
- Provisioned — the module requires a provision and is bound to a named node-scoped provider (the first axis above). This should be the default; on the mesh being migrated onto, most per-module databases are vanilla servers on old version pins that could simply be consolidated onto the node's shared provider.
- Embedded and private — the module ships its own instance because a fork or version genuinely forces it. Its credential is still a mesh-generated secret, not a module-authored password; but the instance is module-internal — same module network only, no published port, not registered as a provision, so nothing else can bind to it and it cannot collide on a well-known port.
The model has no word for the second case. A module that carries a private instance looks, to the mesh, either like nothing (an undeclared container) or like a provider it must not be treated as. "Embed only when a fork or version forces it; otherwise provision against the node's provider" is the rule the design should be able to state and check — and the invisibility of an embedded instance (own network, no host port, not a provision) should be an enforceable property, not a convention.
Open questions
- Where does the provider name live — on the provision definition (
scope: "node"with a provider identity), on the requirement in the consumer's manifest, or supplied only at assignment time so the same module can be bound to different providers on different assignments? - Is "mesh-scoped" still a legitimate scope for some provisions (a single mesh CA, say), or does every provision become node-scoped, with a single instance expressed as "there happens to be one"?
- What is the default when a consumer names no provider — bind to the node the consumer is assigned to (co-located provider), require the name always, or fall back to a mesh-wide default provider where one is declared?
- How does a provider's identity survive being moved between nodes, so a consumer's recorded choice does not silently rebind when the provider relocates?
- Does this interact with secret rotation (the issue-scope of
rotate) — must rotation address a specific provider's credential holders rather than "the provision's"? - When a module carries an embedded, private instance rather than consuming a provider, how is that declared so the mesh knows it is module-internal — not a provision, not published, not bindable by anything else — and can enforce it?
- What decides embed-vs-provision — is it the module's declaration alone, or may an operator override at assignment (consolidate this one onto the node's provider)?