--- status: accepted date: 2026-08-27 deciders: jochen reconstructed: false extends: 0030-the-repository-structure.md --- # 48. The substrate is named ## Context [The substrate](../03-DESIGN/01-to-be/07-the-substrate.md) is defined by a test — *what the control plane consumes and cannot grant itself* — and the design layer describes its members entirely by role: a relational store, a message bus, an object store, an image registry. **No design document names a product.** Postgres appears in zero of them. The names occur only in the as-is layer and in research, describing what already runs. That is a gap rather than a discipline. The rule it came from — [`01-RESEARCH`](../01-RESEARCH/README.md)'s *research never identifies the mesh it observed* — is about node names and domains, not about software. Nothing is protected by declining to write *Postgres* in a public repository, and something is lost: a design that never names a product does not record that the choice was made. Two costs, both already accrued: - **`substrate.lock` cannot be written from the design.** It pins images by digest, and a digest belongs to a named image. - **A reader cannot tell a settled choice from an unexamined one.** "A relational store" reads the same whether the store was chosen deliberately or never considered. ## Decision **The substrate is named, and the names are these:** | Role | Product | Why | |---|---|---| | relational store | **PostgreSQL** | In use, understood, and the provisioning model already assumes its notions of database, role and schema. | | message bus | **LavinMQ** | In use, speaks AMQP, which is what [ADR 0001](0001-nodes-communicate-over-a-broker.md) assumes. Interchangeable with other AMQP brokers at the protocol level, which is what makes it a safe choice rather than a locked-in one. | | object store | **MinIO** | In use, speaks the S3 protocol, which is the closest thing to a portable object-store interface. | | image registry | **the OCI distribution registry** | In use, and the format is the standard rather than a vendor's. | | container runtime | **Docker or Podman** — *detected, not chosen* | See below. The other four rows name one product; this one names two, and the difference is the point. | ### Outside the substrate The gap is not only the substrate's. The design layer names roles for these too, and the same correction applies — a role is a legitimate abstraction, but the product belongs beside it: | Role, as the design says it | Product | Tier | |---|---|---| | **the forge** | **Gitea** | a hosted workload — the mesh builds from it but does not need it to run | | **the coordinator** | the mesh's own pipeline | tier 2 — part of the control plane, not a product | | **ingress** — *exposure*, *certificates* | **Traefik** | not substrate — [ADR 0049](0049-a-route-is-a-grant.md) | **Ingress was a real gap rather than a naming one**, and it is closed by [ADR 0049](0049-a-route-is-a-grant.md): applying the same test shows it is **not** substrate, and a route is an ordinary grant. Recorded here because finding it was the point — naming the products is what made the unnamed role visible. **The container runtime is the one row that is not a choice at all**, and it stopped being one after this record was written ([ADR 0060](0060-the-host-is-built-per-operating-system.md)). The host detects what the machine has and uses it, because adoption keeps a machine's existing configuration rather than replacing it — so naming a single runtime here contradicted a rule already decided. Both are supported, checked against a real podman: only the version probe differs, and one behavioural difference (podman has no daemon, so containers do not return after a reboot unless `podman-restart.service` is enabled) belongs in the declaration rather than the host. **Identity is deliberately absent.** Whether an identity provider is substrate at all depends on whether the control plane delegates authentication, which is undecided ([`07-the-substrate.md`](../03-DESIGN/01-to-be/07-the-substrate.md)). Naming a product before deciding whether the role exists would be the mistake this record is correcting, in reverse. **The role and the product are both written.** A design says *the relational store (PostgreSQL)* rather than one or the other. The role is what the argument turns on; the product is what gets installed, and a reader needs both. ## Consequences - **`substrate.lock` becomes writable.** It pins named images by digest, which was impossible while the design refused to say which images. - **Continuity is the argument, and it is a real one.** Every choice here is what already runs. Nothing was re-litigated, because nothing about the new shape gives a reason to — and changing a substrate service is a migration of the mesh's own state, which is not a cost to pay for novelty. - **Protocols, not products, are what the design depends on.** The bus is reached over AMQP, the object store over S3, the registry over the OCI protocol. Replacing a product is then a substrate migration rather than a redesign — which is the property that makes naming them safe rather than a commitment that cannot be revisited. - **The relational store is the exception**, and it should be said. The provisioning model uses databases, roles and schemas as Postgres means them, and [ADR 0044](0044-a-module-declares-presence-instantiation-and-exclusion.md) already records that two stores from different vendors are not substitutable for a consumer. Replacing it is not a swap. - **The rule that caused this is narrowed, not repealed.** Research still does not identify the mesh it observed — node names, domains, addresses. Product names were never in scope, and the over-application cost the design layer its concreteness. ## References - [`07-the-substrate.md`](../03-DESIGN/01-to-be/07-the-substrate.md) — the test these satisfy. - [ADR 0001](0001-nodes-communicate-over-a-broker.md) — why the bus speaks AMQP. - [ADR 0046](0046-the-installer-fetches-what-it-pins.md) — pinning by digest, which needs a name. - [ADR 0044](0044-a-module-declares-presence-instantiation-and-exclusion.md) — why the store is the one that cannot simply be swapped.