diff --git a/02-DECISIONS/0048-the-substrate-is-named.md b/02-DECISIONS/0048-the-substrate-is-named.md new file mode 100644 index 0000000..1652214 --- /dev/null +++ b/02-DECISIONS/0048-the-substrate-is-named.md @@ -0,0 +1,99 @@ +--- +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** | In use. Podman is the plausible alternative and was not chosen for any deficiency — Docker is what the machines run today and what the current tooling assumes. | + +### 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** | see below | + +**Ingress is a real gap rather than a naming one, and this record does not close it.** The +connectivity context lists *exposure* and *certificates* among its responsibilities, and no +design document says what terminates TLS, how a route reaches a container, or which tier that +belongs to. Traefik is what does it today. Whether it is substrate turns on the same test — can +the control plane grant itself a route? — and nobody has applied the test. **Named here so the +gap is visible; left open because naming it is not answering it.** + +**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. diff --git a/03-DESIGN/01-to-be/01-end-to-end-testing.md b/03-DESIGN/01-to-be/01-end-to-end-testing.md index e73a48d..23b1474 100644 --- a/03-DESIGN/01-to-be/01-end-to-end-testing.md +++ b/03-DESIGN/01-to-be/01-end-to-end-testing.md @@ -35,9 +35,9 @@ today, that is a gap in the vocabulary rather than a reason to privilege that sh ## Two classes of scenario -The design below describes a scenario as a complete mesh — forge, coordinator, delivery cascade -— because what it tests is a module. **That is the larger of two classes, and not the first one -built** ([ADR 0029](../../02-DECISIONS/0029-the-labs-first-scenario-has-no-pipeline.md)). +The design below describes a scenario as a complete mesh — forge (Gitea), coordinator, +delivery cascade — because what it tests is a module. **That is the larger of two classes, and +not the first one built** ([ADR 0029](../../02-DECISIONS/0029-the-labs-first-scenario-has-no-pipeline.md)). | | **Bootstrap scenario** | **Full scenario** | |---|---|---| diff --git a/03-DESIGN/01-to-be/06-the-control-plane.md b/03-DESIGN/01-to-be/06-the-control-plane.md index 6a123cf..5a4ff82 100644 --- a/03-DESIGN/01-to-be/06-the-control-plane.md +++ b/03-DESIGN/01-to-be/06-the-control-plane.md @@ -2,7 +2,7 @@ layer: to-be status: designed code: [] -updated: 2026-08-26 +updated: 2026-08-27 decisions: - 02-DECISIONS/0037-the-host-applies-it-does-not-decide.md - 02-DECISIONS/0030-the-repository-structure.md @@ -71,15 +71,16 @@ in front of them. node except through the host. - **Not a surface.** Tier 3 is how people and agents reach it. It has one interface; the surfaces are what speak to that interface. -- **Not the substrate.** It *runs on* tier 1 — a store, a broker, an object store, a registry — - and cannot start without them, which is what makes them a lower tier. +- **Not the substrate.** It *runs on* tier 1 — PostgreSQL, LavinMQ, MinIO, an OCI registry + ([ADR 0048](../../02-DECISIONS/0048-the-substrate-is-named.md)) — and cannot start without + them, which is what makes them a lower tier. - **Not privileged on a node.** It has no more access to a machine than the declaration vocabulary allows ([ADR 0039](../../02-DECISIONS/0039-the-link-is-the-security-boundary.md)). ## It is also a consumer The property that makes tier 2 unlike the others: **the control plane has requirements of its -own.** It needs a database, a broker, and somewhere to keep artifacts — the same things any +own.** It needs a PostgreSQL database, an AMQP virtual host, and a bucket — the same things any module needs, granted the same way. That is the circularity the tiers exist to resolve rather than hide: the control plane cannot diff --git a/03-DESIGN/01-to-be/07-the-substrate.md b/03-DESIGN/01-to-be/07-the-substrate.md index 1397df9..ef61c03 100644 --- a/03-DESIGN/01-to-be/07-the-substrate.md +++ b/03-DESIGN/01-to-be/07-the-substrate.md @@ -2,11 +2,14 @@ layer: to-be status: designed code: [] -updated: 2026-08-26 +updated: 2026-08-27 decisions: - 02-DECISIONS/0030-the-repository-structure.md - 02-DECISIONS/0037-the-host-applies-it-does-not-decide.md - 02-DECISIONS/0038-a-node-joins-by-linking-first.md + - 02-DECISIONS/0046-the-installer-fetches-what-it-pins.md + - 02-DECISIONS/0047-the-bundle-may-carry-actions-the-link-may-not.md + - 02-DECISIONS/0048-the-substrate-is-named.md --- # The substrate @@ -28,13 +31,25 @@ The test, applied: | | control plane needs it | can it grant itself one? | | |---|---|---|---| -| a relational store | its own state lives there | no — provisioning needs the store | **substrate** | -| a message bus | it reaches nodes over it ([ADR 0001](../../02-DECISIONS/0001-nodes-communicate-over-a-broker.md)) | no — it cannot grant itself a virtual host | **substrate** | -| an object store | artifacts and blobs it delivers | no — it needs a bucket to hold them | **substrate** | -| an image registry | images it delivers to nodes | no — it needs a repository | **substrate** | +| a relational store — **PostgreSQL** | its own state lives there | no — provisioning needs the store | **substrate** | +| a message bus — **LavinMQ** | it reaches nodes over it ([ADR 0001](../../02-DECISIONS/0001-nodes-communicate-over-a-broker.md)) | no — it cannot grant itself a virtual host | **substrate** | +| an object store — **MinIO** | artifacts and blobs it delivers | no — it needs a bucket to hold them | **substrate** | +| an image registry — **the OCI registry** | images it delivers to nodes | no — it needs a repository | **substrate** | | an identity provider | only if it delegates authentication | — | **conditional, below** | | anything else the mesh hosts | no | — | not substrate | +**The role and the product are both written**, here and everywhere +([ADR 0048](../../02-DECISIONS/0048-the-substrate-is-named.md)). The role is what the argument +turns on — the test above works on roles, and would give the same answers for a different store. +The product is what actually gets installed and pinned, and a design that names only the role +does not record that the choice was ever made. + +The dependency is on the **protocol**, not the product: AMQP for the bus, S3 for the object +store, the OCI protocol for the registry. That is what keeps the naming safe rather than a +commitment that cannot be revisited — replacing one is a substrate migration, not a redesign. +The store is the exception, and the exception matters: the provisioning model uses databases, +roles and schemas as PostgreSQL means them, so it is the one member that is not a swap. + ## What that resolves **Four or five?** [Research 006](../../01-RESEARCH/006-mesh-from-scratch/00-overview.md) asks @@ -67,8 +82,24 @@ cannot obtain it* is. ## The pinned bundle -The substrate is what `substrate.lock` contains, and this is the only place in the mesh where -versions are pinned by hand rather than resolved. +`substrate.lock` holds **what must exist before the control plane runs** — which is a smaller +set than the substrate, and the difference is easy to miss. It is the only place in the mesh +where versions are pinned by hand rather than resolved. + +Being substrate and being in the bundle are two different questions: + +| | is it substrate? | must it precede the control plane? | +|---|---|---| +| PostgreSQL | yes — the control plane's own state lives in it | **yes** — there is nowhere to put that state otherwise | +| LavinMQ | yes — it cannot grant itself a virtual host | **not established** — see below | +| MinIO | yes — it cannot grant itself a bucket | no — nothing is delivered before the mesh exists | +| the OCI registry | yes — it cannot grant itself a repository | no — the first node fetches upstream ([ADR 0046](../../02-DECISIONS/0046-the-installer-fetches-what-it-pins.md)) | + +The three on the bottom rows are **substrate by role and ordinary by delivery**: by the time +they are wanted there is a control plane, and it provisions them the way it provisions anything. +That keeps the bundle to roughly one image rather than four, which is what makes it small enough +for the review [ADR 0047](../../02-DECISIONS/0047-the-bundle-may-carry-actions-the-link-may-not.md) +requires. **Why pinned:** the bundle is applied when no mesh exists, so nothing can resolve a version, ask a registry, or check a constraint. What the host carries must already be exact. @@ -85,17 +116,21 @@ is what keeps the bundle small enough for a person to read and check. The order, from [research 011](../../01-RESEARCH/011-the-module-graph/worked-provider.md): ``` -0 a container runtime exists detected, or installed as a package -1 the store runs pulled by digest, from the bundle +0 Docker exists detected, or installed as a package +1 PostgreSQL runs pulled by digest, from the bundle 2 a database is created in it an action, run locally 3 the control plane's schema applied an action, against that database 4 the control plane starts and only now is there a mesh -5 everything else is provisioned the ordinary path +5 LavinMQ, MinIO, the registry, and the ordinary path + everything else are provisioned ``` +Only PostgreSQL is raised from the bundle, for the reason in *The pinned bundle* above — the +rest of the substrate is wanted only once there is a control plane to provision it. + **Step 0 is easy to leave out and it is where several things meet.** A substrate service is a -container, so something must run containers before anything else happens — and a container -runtime is a *package*, not a container. It is: +container, so Docker must be running before anything else happens — and Docker is a *package*, +not a container. It is: - what the host's capability detection already reports, and the first use of that report by something other than a person; @@ -104,7 +139,7 @@ runtime is a *package*, not a container. It is: - a package, which needs the machine's own package manager and a network — both permitted by [ADR 0046](../../02-DECISIONS/0046-the-installer-fetches-what-it-pins.md). -So the host's bootstrap vocabulary is five shapes: **package**, **container**, **file**, +So the host's bootstrap vocabulary is six shapes: **package**, **container**, **file**, **directory**, **service**, and **action**. Files, directories and services exist; the rest do not yet. @@ -117,6 +152,14 @@ host's vocabulary grows by one shape rather than by one resource type per substr ## Open - **Whether identity is the fifth.** Above; it follows from a decision not yet taken. +- **Whether the bus must precede the control plane.** The bundle table marks this *not + established*, and it is the one row that could still move. The control plane reaches nodes over + AMQP, but at step 4 there is exactly one node and it is the local machine — so whether LavinMQ + is needed to *start* or only to *reach a second node* depends on whether the control plane's own + contexts talk to each other over the bus. If they do, LavinMQ joins PostgreSQL in the bundle and + the bootstrap grows a step; if they do not, it is provisioned like anything else. **This is a + question about the control plane's internal shape, not about the substrate**, which is why it is + not answered here. - ~~**Whether the host can do step 2.**~~ **Resolved** by [ADR 0047](../../02-DECISIONS/0047-the-bundle-may-carry-actions-the-link-may-not.md). A service running on this machine is part of this machine, so the scope was never in question — the real