**0027 — provisions.** A module written against PostgreSQL could be matched to a provider of SQL Server, resolve as satisfied, and fail on its first query. The name said the role, so nothing distinguished engines. Refusing on ambiguity could not help: with one provider of each name nothing is ambiguous. Enforced at parse rather than documented, because the old naming was the documentation. **0028 — the substrate.** 0006 admits an object store on the grounds that it cannot grant itself a bucket. That answers the second half of the test and assumes the first: the control plane does not need one. Verified — no S3 client in mesh-control, and internal/builder/registry.go records the deliberate choice to put artifacts in the OCI registry as content-addressed blobs. The row was inherited from the system being replaced, where an object store distributed module tarballs, and was never re-tested against the definition above it. So an object store is an ordinary module, and a mesh with nothing needing one runs none. Migrating it is module work, not substrate work. 0028 also states what 0006 left unsaid: a substrate service and a module of the same product are different instances. The substrate is raised from the bundle before any mesh exists, so it is not in the module graph — a workload depending on it would depend on something the graph cannot see, cannot rotate a credential for, and cannot move, and would put workload data in the store the control plane keeps its own state in. Both records were found by reading code against design rather than design against itself, which is the review that should have happened sooner.
5.9 KiB
topic, status, date, deciders, reconstructed, extends
| topic | status | date | deciders | reconstructed | extends |
|---|---|---|---|---|---|
| the tiers | accepted | 2026-08-31 | jochen | false | 02-DECISIONS/0006-the-substrate-and-the-control-plane.md |
28. The substrate supplies the control plane and nothing else
Corrects one row of ADR 0006 and makes explicit something it left unsaid. The rest of that record stands.
Context
ADR 0006 defines the substrate by a circularity: what the control plane needs in order to run, and cannot ask itself for, because it is not running yet. Two questions, and both must be answered yes for something to be substrate.
Its membership table admits the object store on this line:
| role | product | |
|---|---|---|
| object store | MinIO | it cannot grant itself a bucket |
That answers the second question and assumes the first. It is true that a control plane cannot grant itself a bucket. Nothing establishes that it needs one.
It does not. Verified 2026-08-31 against mesh-control: no S3 client, no bucket, no object
storage of any kind outside comments. Artifacts reach nodes as content-addressed blobs in the OCI
registry, and the code records the decision and its reasoning:
One store, and it is the registry the bootstrap already pulls from. An OCI registry is a content-addressed blob store that happens to also understand images… The alternative considered was a second store beside it — S3-shaped, buckets, signed URLs. It is the right answer for objects that are mutable, or need per-reader access, or are not build output. None of that describes a digest-pinned archive, and standing up a second service to hold one kind of immutable blob means two things to run, two things to back up and two ways for an artifact to be missing.
The row is inherited from the system being replaced, where an object store distributed module tarballs. Here nothing does, and the row was never re-tested against the definition it sits under.
A second thing ADR 0006 never says: whether a substrate service and a module of the same product are the same instance. It says the substrate is not the control plane and not a place for logic, and stops. The question is not idle — an application wanting a database, on a mesh whose substrate is already running PostgreSQL, has an obvious wrong answer available.
Considered Options
-
Leave the object store as substrate, unused. Harmless-looking. Rejected. A membership list that includes something nothing needs is a list that has stopped being derived from its test, and the next member is admitted by precedent instead of argument. It also mandates that every mesh run a service no mesh uses.
-
Applications share the substrate's instances. One PostgreSQL, one of everything. Rejected, below.
-
The substrate is exactly what the control plane consumes; everything else is a module. Adopted.
Decision
The object store is not substrate. It fails the first half of the test: the control plane does not need one. An object store is an ordinary module, required through the module graph like anything else, and a module wanting one depends on a module providing one.
The substrate has four members, not five: a relational store, a message bus, an image registry, and conditionally an identity provider. The registry stays — the control plane genuinely cannot deliver an artifact without somewhere to put it.
A substrate service and a module of the same product are different instances, and are not shared. The mesh's own PostgreSQL and a PostgreSQL a workload was given are two servers, two containers, two lifecycles.
Three reasons, and the first is the one that matters:
The substrate is not in the module graph. It is raised from the pinned bundle the host carries, before any mesh exists to declare it. A workload depending on it would depend on something the graph cannot see, cannot rotate a credential for, and cannot move — which is every property the provisioning model exists to provide.
It would put workload data in the control plane's own store. The mesh's contexts own their stores exclusively (ADR 0008). An application sharing that server can exhaust it, lock it, or fill its disk, and the failure is the control plane going down — which is the one failure that makes every other one harder to fix.
They are bounded differently. The substrate is sized, backed up and upgraded as part of bootstrapping a mesh. A workload's database follows the workload — moved with it, destroyed with it, restored with it.
Consequences
Migrating an object store is ordinary module work, not substrate work. It was previously going to be done as part of completing the substrate, which would have been the wrong shape and would have coupled every mesh to a service the mesh does not use.
A mesh with no workload needing one runs no object store at all. That is the correct outcome and was not previously available.
Two PostgreSQL containers on a node that hosts both is expected, not duplication to be optimised away. Anyone tidying them together should find this record first.
"Substrate by role and ordinary by delivery" loses one of its two members. ADR 0006 uses that phrase of the object store and the registry — things that are substrate but provisioned once a control plane exists. It now describes the registry alone.
The definition is applied, not just stated. Both halves of the circularity test are asked of each member, and cannot grant itself one is not sufficient on its own — it is true of almost any service, which is what made it possible to admit a member on that half alone.