All six shapes are built. 07 still said the last three did not exist, and 05 still described stage 2 as having built three of six. Records what the lab still cannot do, because that is now the only thing between here and an end-to-end substrate bootstrap: a sealed scenario cannot fetch an image and its machines carry no container runtime, so package, container and action were verified against a real machine instead.
176 lines
10 KiB
Markdown
176 lines
10 KiB
Markdown
---
|
|
layer: to-be
|
|
status: designed
|
|
code: []
|
|
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
|
|
- 02-DECISIONS/0049-a-route-is-a-grant.md
|
|
---
|
|
|
|
# The substrate
|
|
|
|
Tier 1. Defined the same way [the control plane](06-the-control-plane.md) is, because the same
|
|
gap applied: the word was load-bearing and unpinned.
|
|
|
|
## The definition
|
|
|
|
> **The substrate is what the control plane consumes and cannot grant itself.**
|
|
|
|
Every module that needs a database asks the control plane's provisioning for one. The control
|
|
plane needs a database too — and it cannot ask itself, because it is not running yet. That
|
|
circularity is not an awkwardness to work around; it *is* the definition. Anything on the wrong
|
|
side of it must be raised some other way, and the other way is the bundle the host carries
|
|
([ADR 0038](../../02-DECISIONS/0038-a-node-joins-by-linking-first.md)).
|
|
|
|
The test, applied:
|
|
|
|
| | control plane needs it | can it grant itself one? | |
|
|
|---|---|---|---|
|
|
| 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** |
|
|
| ingress — **Traefik** | not to start; only to be reached by name | — it grants itself one afterwards | **not substrate** ([ADR 0049](../../02-DECISIONS/0049-a-route-is-a-grant.md)) |
|
|
| 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
|
|
whether the identity provider is a substrate service, and the test answers it *conditionally* —
|
|
which is the honest answer rather than a number.
|
|
|
|
- If the control plane **delegates** authentication, it cannot serve anybody before the provider
|
|
exists, and it cannot grant itself a client. **Substrate.**
|
|
- If it **authenticates natively**, the provider is an ordinary hosted service like any other.
|
|
**Not substrate.**
|
|
|
|
So the count follows from a design decision that has not been taken, and the record should say
|
|
that rather than assert four.
|
|
|
|
**Why not "important infrastructure".** An identity provider, a mail server and an analytics
|
|
service are all infrastructure by any ordinary reading, and none of them are substrate — the
|
|
control plane starts and runs without them. *Important* is not the test; *the control plane
|
|
cannot obtain it* is.
|
|
|
|
## What the substrate is not
|
|
|
|
- **Not tier 0.** The host raises the substrate; it is not part of it. The host carries the
|
|
declaration that brings the substrate up, and depends on nothing.
|
|
- **Not the control plane.** These are services with no knowledge of the mesh. A store does not
|
|
know what a node is.
|
|
- **Not a place for logic.** The skeleton is explicit: tier 1 is *declarations only, no logic of
|
|
its own.* A substrate service is an upstream image, pinned, with configuration.
|
|
- **Not privileged.** The substrate is provisioned *from* by the control plane and grants
|
|
nothing on its own initiative.
|
|
|
|
## The pinned bundle
|
|
|
|
`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.
|
|
|
|
**Why references and not payload:** the bundle names images by **digest** and the host fetches
|
|
them ([ADR 0046](../../02-DECISIONS/0046-the-installer-fetches-what-it-pins.md)). A first node is
|
|
a real machine with a network; the sealed case is the lab, and the lab places images itself.
|
|
|
|
Reproducibility comes from pinning the identity of a thing rather than carrying its bytes, which
|
|
is what keeps the bundle small enough for a person to read and check.
|
|
|
|
## Raising it
|
|
|
|
The order, from [research 011](../../01-RESEARCH/011-the-module-graph/worked-provider.md):
|
|
|
|
```
|
|
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 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 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;
|
|
- **adopted rather than installed** when the machine already has one with configuration somebody
|
|
chose ([research 012](../../01-RESEARCH/012-the-minimum-viable-node/00-overview.md));
|
|
- 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 six shapes: **package**, **container**, **file**,
|
|
**directory**, **service**, and **action**. **All six are built**
|
|
([`05-the-node-host.md`](05-the-node-host.md) stage 2), so nothing in this bootstrap is
|
|
blocked on the host any longer.
|
|
|
|
**Steps 2 and 3 happen before there is a mesh to do them**, which is why provisioning is part of
|
|
the bootstrap rather than a service consumers use later. They are **actions** the bundle
|
|
declares and the host runs
|
|
([ADR 0047](../../02-DECISIONS/0047-the-bundle-may-carry-actions-the-link-may-not.md)) — so the
|
|
host's vocabulary grows by one shape rather than by one resource type per substrate service.
|
|
|
|
## 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
|
|
question was whether the host must learn what a database is, and it must not. The bundle
|
|
declares an **action**; the host runs it and verifies it, and what a database means stays with
|
|
the module that provides one.
|
|
- **Whether one host can raise all four.** The claim under stage 2 of
|
|
[the node host](05-the-node-host.md), never proved. If it is false, the tier boundary moves.
|
|
- **How the substrate is updated once a mesh exists.** Pinned by hand at bootstrap; afterwards
|
|
the control plane could deliver it like anything else, and nothing says whether it does.
|