Adopt the glossary's vocabulary in the mutable design docs
"control plane" -> controller and "substrate" -> foundation throughout 03-DESIGN, 00-META and the README, with 06-the-control-plane.md and 07-the-substrate.md renamed to 06-the-controller.md and 07-the-foundation.md. The immutable 02-DECISIONS records keep their original wording (and links to them are unchanged) — a term retired here may still appear there, which the glossary explains how to read. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
---
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code:
|
||||
- mesh-host examples/foundation-first-node.lock
|
||||
- mesh-host internal/apply
|
||||
- mesh-lab test/integration/mesh.test.ts (a bare machine becomes a mesh)
|
||||
updated: 2026-08-31
|
||||
decisions:
|
||||
- 02-DECISIONS/0004-a-node-and-how-it-joins.md
|
||||
- 02-DECISIONS/0005-the-node-host.md
|
||||
- 02-DECISIONS/0006-the-substrate-and-the-control-plane.md
|
||||
- 02-DECISIONS/0007-connectivity.md
|
||||
- 02-DECISIONS/0008-a-context-owns-its-store.md
|
||||
- 02-DECISIONS/0019-how-this-repository-works.md
|
||||
---
|
||||
|
||||
# The foundation
|
||||
|
||||
Tier 1. Defined the same way [the controller](06-the-controller.md) is, because the same
|
||||
gap applied: the word was load-bearing and unpinned.
|
||||
|
||||
## The definition
|
||||
|
||||
> **The foundation is what the controller consumes and cannot grant itself.**
|
||||
|
||||
Every module that needs a database asks the controller'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 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)).
|
||||
|
||||
The test, applied:
|
||||
|
||||
| | controller needs it | can it grant itself one? | |
|
||||
|---|---|---|---|
|
||||
| a relational store — **PostgreSQL** | its own state lives there | no — provisioning needs the store | **foundation** |
|
||||
| a message bus — **LavinMQ** | it reaches nodes over it ([ADR 0002](../../02-DECISIONS/0002-nodes-communicate-over-a-broker.md)) | no — it cannot grant itself a virtual host | **foundation** |
|
||||
| ~~an object store~~ | ~~artifacts and blobs it delivers~~ | — | **not foundation** — [ADR 0028](../../02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md) |
|
||||
| ~~an image registry~~ | ~~images it delivers to nodes~~ | — | **not foundation** — needed to operate, not to start ([ADR 0033](../../02-DECISIONS/0033-the-substrate-is-a-store-and-a-broker.md)) |
|
||||
| ~~an identity provider~~ | ~~only if it delegates authentication~~ | — | **not foundation** — it delegates to nothing ([ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md)) |
|
||||
| ingress — **Traefik** | not to start; only to be reached by name | — it grants itself one afterwards | **not foundation** ([ADR 0007](../../02-DECISIONS/0007-connectivity.md)) |
|
||||
| anything else the mesh hosts | no | — | not foundation |
|
||||
|
||||
*The object-store row was wrong, and how it was wrong is worth keeping.* It answered *can it
|
||||
grant itself one* — no, it cannot grant itself a bucket — while assuming the first column. **The
|
||||
controller does not need an object store**: it has no S3 client and never has, and artifacts
|
||||
reach nodes as content-addressed blobs in the registry. The row was inherited from the system being
|
||||
replaced, where an object store distributed module tarballs, and was never re-tested against the
|
||||
definition above it. *Both columns must be answered, and the second is true of almost any service.*
|
||||
|
||||
**An object store is an ordinary module**, required through the module graph by whatever wants one.
|
||||
A mesh with no workload needing one runs none.
|
||||
|
||||
**The role and the product are both written**, here and everywhere
|
||||
([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.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 foundation 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 foundation service, and the test answers it *conditionally* —
|
||||
which is the honest answer rather than a number.
|
||||
|
||||
- If the controller **delegates** authentication, it cannot serve anybody before the provider
|
||||
exists, and it cannot grant itself a client. **Foundation.**
|
||||
- If it **authenticates natively**, the provider is an ordinary hosted service like any other.
|
||||
**Not foundation.**
|
||||
|
||||
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 foundation — the
|
||||
controller starts and runs without them. *Important* is not the test; *the controller
|
||||
cannot obtain it* is.
|
||||
|
||||
## What the foundation is not
|
||||
|
||||
- **Not tier 0.** The host raises the foundation; it is not part of it. The host carries the
|
||||
declaration that brings the foundation up, and depends on nothing.
|
||||
- **Not the controller.** 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 foundation service is an upstream image, pinned, with configuration.
|
||||
- **Not privileged.** The foundation is provisioned *from* by the controller and grants
|
||||
nothing on its own initiative.
|
||||
- **Not the mesh's supply of anything**
|
||||
([ADR 0028](../../02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md)).
|
||||
A foundation service and a module of the same product are **different instances**. The mesh's own
|
||||
PostgreSQL and a PostgreSQL a workload was given are two servers, and a node hosting both runs
|
||||
two containers — expected, not duplication to be tidied away.
|
||||
|
||||
The foundation is raised from the bundle before any mesh exists, so **it is not in the module
|
||||
graph**: a workload depending on it would depend on something the graph cannot see, cannot rotate
|
||||
a credential for, and cannot move. It would also put workload data in the store the controller
|
||||
keeps its own state in, where a workload that fills a disk takes down the one thing needed to fix
|
||||
it.
|
||||
|
||||
## The pinned bundle
|
||||
|
||||
`foundation.lock` holds **what must exist before the controller runs** — which is a smaller
|
||||
set than the foundation, 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 foundation and being in the bundle are two different questions:
|
||||
|
||||
| | is it foundation? | must it precede the controller? |
|
||||
|---|---|---|
|
||||
| PostgreSQL | yes — the controller's own state lives in it | **yes** — there is nowhere to put that state otherwise |
|
||||
| LavinMQ | yes — it cannot grant itself a virtual host | **yes** — the controller reaches a node only over the link, and the link is the broker ([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)) |
|
||||
| the OCI registry | yes — it cannot grant itself a repository | no — the first node fetches upstream ([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)) |
|
||||
|
||||
The registry is **foundation by role and ordinary by delivery**: by the time it is wanted there is
|
||||
a controller, and it provisions it the way it provisions anything. That keeps the bundle small
|
||||
enough for the review [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) requires — one
|
||||
foundation image until
|
||||
[ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md) established that the
|
||||
broker has to precede the controller, and two since.
|
||||
|
||||
*Corrected 2026-08-31, from counting what the bundle holds rather than reasoning about it.* **It
|
||||
carries three images, not two** — PostgreSQL, LavinMQ, and the controller itself, which the
|
||||
sentence above had overlooked by counting only foundation services. The controller is what the
|
||||
foundation exists to start, and it is in the bundle for the same reason they are: there is nothing
|
||||
to fetch it with yet. It also carries seven actions, a package and a service.
|
||||
|
||||
**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 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.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 per context is created an action, run locally — one today, `inventory`
|
||||
3 each context's schema is applied an action, against its own database
|
||||
4 LavinMQ runs pulled by digest, from the bundle
|
||||
5 a virtual host, a credential, and actions, run locally
|
||||
a self-signed certificate
|
||||
6 the controller starts and only now is there a mesh
|
||||
7 the registry, and everything else the ordinary path
|
||||
are provisioned
|
||||
```
|
||||
|
||||
**Steps 4 and 5 are why the bundle is not one image**
|
||||
([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)). The controller cannot
|
||||
provision the broker, because provisioning means telling a host, and telling a host happens over
|
||||
the broker. The first node does not escape this by being local: it enrols the ordinary way, by
|
||||
dialling the broker at the address in its token.
|
||||
|
||||
**Step 2 is one database per context and not one called `mesh`.** A context is granted only what it
|
||||
exclusively owns ([ADR 0008](../../02-DECISIONS/0008-a-context-owns-its-store.md)), *the mesh
|
||||
database* names a thing that will not exist
|
||||
([ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md)), and a separate
|
||||
database is a boundary a cross-context join cannot casually cross where a separate schema is not.
|
||||
|
||||
Only PostgreSQL is raised from the bundle, for the reason in *The pinned bundle* above — the
|
||||
rest of the foundation is wanted only once there is a controller to provision it.
|
||||
|
||||
**Step 0 is easy to leave out and it is where several things meet.** A foundation 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 0005](../../02-DECISIONS/0005-the-node-host.md)): a machine that
|
||||
already has one keeps it. On a machine with none, the controller 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 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md).
|
||||
|
||||
So the bootstrap uses four shapes: **package**, **container**, **service** and **action** —
|
||||
*counted from `foundation-first-node.lock`, which is the only bundle there is*. It had said six,
|
||||
adding `file` and `directory`, which this bootstrap never asks for.
|
||||
|
||||
All four are built, as are the host's other five
|
||||
([`05-the-node-host.md`](05-the-node-host.md) stage 2), so nothing in this bootstrap is blocked
|
||||
on the host any longer — which is the claim that mattered, and it was true either way.
|
||||
|
||||
**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 0005](../../02-DECISIONS/0005-the-node-host.md)) — so the
|
||||
host's vocabulary grows by one shape rather than by one resource type per foundation service.
|
||||
|
||||
## Open
|
||||
|
||||
- ~~**Whether identity is the fifth.**~~ **Closed 2026-08-31** by
|
||||
[ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md): the control
|
||||
plane delegates authentication to nothing, so identity is an ordinary module. With the object
|
||||
store gone ([ADR 0028](../../02-DECISIONS/0028-the-substrate-supplies-the-control-plane-and-nothing-else.md))
|
||||
the foundation is three, and no member is conditional.
|
||||
- ~~**Whether the bus must precede the controller.**~~ **Resolved** by
|
||||
[ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md) — it must, and the question as
|
||||
posed here could not have answered it. This asked whether the controller's contexts talk to
|
||||
each other over the bus; they do not, being one process, which under this framing would have
|
||||
kept LavinMQ out of the bundle. What decides it is how the controller reaches a *node*, which
|
||||
is only ever over the link.
|
||||
- **What issues the broker's certificate at bootstrap.** New, and created by the row above. A
|
||||
token pins the fingerprint a host must expect before it sends anything
|
||||
([`09-the-node-lifecycle.md`](09-the-node-lifecycle.md)), so the broker needs a certificate at a
|
||||
moment when there is no mesh to issue one and no public name to obtain one for. Self-signed and
|
||||
pinned is the shape that fits; how it is later replaced by the certificates in
|
||||
[`08-connectivity.md`](08-connectivity.md) is not decided.
|
||||
- **How a context added later gets its database.** By then there is a controller — but one
|
||||
holding a credential that can create databases holds more than what it exclusively owns
|
||||
([ADR 0008](../../02-DECISIONS/0008-a-context-owns-its-store.md)).
|
||||
- ~~**Whether the host can do step 2.**~~ **Resolved** by
|
||||
[ADR 0005](../../02-DECISIONS/0005-the-node-host.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 three.** 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 foundation is updated once a mesh exists.** Pinned by hand at bootstrap; afterwards
|
||||
the controller could deliver it like anything else, and nothing says whether it does.
|
||||
|
||||
## Raised, and observed
|
||||
|
||||
*Written 2026-08-30, the first time a bare machine became a running mesh and something joined it.*
|
||||
|
||||
**It works, and what that means precisely:** a machine with a container runtime and nothing else
|
||||
applied the bundle its host carries and ended with a store, a database per context, those
|
||||
contexts' schemas, a broker holding a certificate it generated itself, and the controller
|
||||
serving on top of them. Eleven resources, one command, no mesh to ask anything of.
|
||||
|
||||
**Then it joined itself.** The same machine took a token, checked the broker against the
|
||||
fingerprint pinned in it, generated three keypairs, and enrolled — which is
|
||||
[ADR 0004](../../02-DECISIONS/0004-a-node-and-how-it-joins.md)'s *the first node is a node whose
|
||||
mesh is not up yet*, observed rather than argued. Its specialness lasted one command.
|
||||
|
||||
**And a credential crossed.** With a second node recorded, the machine was declared the provider
|
||||
of a database and pushed to over the broker. What arrived and what did not is the whole of the
|
||||
[secrets argument](../../02-DECISIONS/0009-modules-and-the-graph.md), measured on a real machine:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| the password, in plain text | **on the machine only**, one file, mode 0600 |
|
||||
| in the declaration that crossed the broker | absent |
|
||||
| in the controller's database | absent |
|
||||
| in what the node reported back | absent |
|
||||
|
||||
**One fault, and it was in the joining.** The token did not say what the mesh calls the machine,
|
||||
so enrolment needed a flag its own help said it did not — and failed at the broker with an empty
|
||||
username. Recorded in ADR 0004 as the fifth thing a token carries.
|
||||
Reference in New Issue
Block a user