A sweep for claims overtaken by the last few days. Annotated rather than rewritten, following the pattern already in 0049 -- what changed and why is the useful part, and an accepted record should not quietly become something else. 0057's init section was wrong on all three of its claims. It said the host needs FOUR things from an init; 0061 reduced that to one. It said every machine the mesh targets already has systemd; Alpine does not, and it is the intended first node. It said there is no second init to abstract over; there is now, and the answer is still not an abstraction -- it is a four-line file per system. What survives is the part that was always right: an init is not a dependency in 0041's sense, because it is not installed, it is what the machine already is. 0048 named Docker as the container runtime. It is now docker or podman, detected rather than chosen -- because adoption keeps what a machine already has, so naming one contradicted a rule already decided. That row is the only one of the five that names two, and the record now says why. 0060 claimed the bundle is portable across operating systems. Its mechanism is; its contents are not -- package names, unit names, service names all differ, so an Arch host embeds an Arch bundle. That was my error, and it is the exact confusion behind the question that found it. The design layer had the same drift: 07 and 09 said "Docker" where they meant a container runtime, 09 said systemd restarts the host after an upgrade when the launcher does, and both install snippets assumed Arch. They now show Alpine and Arch side by side, which makes the point better than prose did -- step 1 differs per system, step 2 never does. Checked and NOT changed: 0047's "the vocabulary grows by one shape" is a claim about the rate, not the count, and is still true. 0037 lists docker among tools the host manages, which it does. 0041 says nothing about either.
182 lines
10 KiB
Markdown
182 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
|
|
- 02-DECISIONS/0060-the-host-is-built-per-operating-system.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 a container runtime exists detected — docker or podman — or installed
|
|
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 a container runtime must be working before anything else happens — and a runtime
|
|
is a *package*, not a container.
|
|
|
|
**Which runtime is detected, not chosen**
|
|
([ADR 0060](../../02-DECISIONS/0060-the-host-is-built-per-operating-system.md)): a machine that
|
|
already has one keeps it. On a machine with none, the control plane names the package, because
|
|
what it is called differs per system. 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.
|