--- topic: the tiers status: accepted date: 2026-08-31 deciders: jochen reconstructed: false extends: 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](0006-the-substrate-and-the-control-plane.md) 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 1. **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. 2. **Applications share the substrate's instances.** One PostgreSQL, one of everything. **Rejected**, below. 3. **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](0008-a-context-owns-its-store.md)). 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. ## References - [ADR 0006](0006-the-substrate-and-the-control-plane.md) — the definition, and the table this corrects one row of - [ADR 0008](0008-a-context-owns-its-store.md) — a context owns its store exclusively - `mesh-control internal/builder/registry.go` — where artifacts go, and why not S3