Files
hq/03-DESIGN/01-to-be/07-the-foundation.md
T
jschoubben ee7abe0a8e ADR 0079: foundation seats are named after their servers; issue 056 resolved
The store and broker were "one per mesh" by convention only. Each foundation module now
claims a mesh-scoped seat named after the server it guards — postgres/mesh-store,
lavinmq/mesh-broker — and the controller's seat is renamed the-controller -> mesh-controller
so all three follow one rule. The resolver refuses a second holder, closing 056. Glossary,
the foundation and installation docs, and the decisions index follow the new name.

https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-17 02:15:08 +02:00

273 lines
17 KiB
Markdown

---
layer: to-be
status: in-progress
code:
- mesh-host examples/foundation-first-node.lock
- mesh-host internal/apply
- mesh-host internal/bootstrap/phase3.go
- mesh-catalog modules/postgres
- mesh-catalog modules/lavinmq
- mesh-lab test/integration/mesh.test.ts (a bare machine becomes a mesh)
updated: 2026-09-17
decisions:
- 02-DECISIONS/0004-a-node-and-how-it-joins.md
- 02-DECISIONS/0078-the-store-and-broker-are-modules.md
- 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
- 02-DECISIONS/0077-the-controller-and-the-foundation.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.