The tiers were settled and the product was named, but the repositories themselves existed only in a research sketch. That had already caused two problems. ADR 0029 makes the lab phase 0 of the migration and could not say where it lives, because no record named a repository. And the sketch contradicted an accepted record: it listed mesh-hq while ADR 0028 had decided novox/hq and explicitly rejected that name. A design resting on research is resting on something that can change without a decision. Corrected in the research too. The naming rule, which both earlier records implied and neither stated: a repository belonging to a product carries that product's prefix; a company-scoped one does not. That is why this repository is hq and the mesh's are mesh-*. Seven repositories recorded — host, substrate, control, surfaces, sdk, lab, and this one. The lab gets its own: it ships to nobody, outlives any single tier, and drives virtualisation on a workstation, which nothing else does. Inside the host it would couple development tooling to a shipped component; inside the control plane the bootstrap scenario would depend on a tier that does not exist when it is needed. Tier 4 is deliberately not decided. Whether the catalogue is one repository, one per domain or one per application stays open from ADR 0015 and is blocked on research 005 — how many repositories hold domains cannot be answered before knowing what the domains are. mesh-catalog appears in the sketch and is not decided by this record. The cost is stated rather than glossed: seven release cadences where there is one, and cross-repository changes that used to be one commit.
273 lines
14 KiB
Markdown
273 lines
14 KiB
Markdown
---
|
|
effort: 006-mesh-from-scratch
|
|
updated: 2026-08-23
|
|
---
|
|
|
|
# The code skeleton
|
|
|
|
[`skeleton.md`](skeleton.md) laid out tiers and repositories. This is the level below: what a
|
|
module looks like on disk, and — the question that forced a correction — where a given piece of
|
|
today's catalogue actually lands.
|
|
|
|
## The tier test
|
|
|
|
A decision procedure, so placement is answerable rather than argued. Ask in order; first match
|
|
wins:
|
|
|
|
1. **Does it apply state on a machine?** → tier 0, inside the host.
|
|
2. **Can the control plane *start* without it?** If *no* → tier 1, substrate. Note the verb:
|
|
*start*, not *function fully*. A capability the control plane loses without something is not
|
|
the same as a thing it cannot come up without — see "becoming self-hosting" below.
|
|
3. **Does it decide what should be true across nodes?** → tier 2, a control-plane context.
|
|
4. **Is it a way to talk to tier 2, holding no logic of its own?** → tier 3, a surface.
|
|
5. **Otherwise** → tier 4, a workload.
|
|
|
|
## Worked example — where postgres ends up
|
|
|
|
The obvious answer is "twice": once as the mesh's own database in the substrate, once as a
|
|
hosted database in the catalogue. That answer is wrong, and seeing why fixes something.
|
|
|
|
Run the test. *Can the control plane exist without a relational store?* No. **Postgres is
|
|
tier 1.** It lives once:
|
|
|
|
```
|
|
mesh-substrate/store/postgres/
|
|
```
|
|
|
|
There is no second copy in the catalogue, because the thing that differs between the mesh's own
|
|
database and a project's database is **not the module**. It is how that instance is brought up:
|
|
|
|
| | The mesh's own instance | A project's database |
|
|
|---|---|---|
|
|
| Brought up by | the host, from the pinned bundle, with no control plane present | the ordinary delivery and provisioning path |
|
|
| Declared in | `mesh-substrate/bundle.yml` | the consuming module's `requires:` |
|
|
| Exists because | the control plane cannot start without it | something asked for it |
|
|
|
|
Same module, two roles. **Tier is a property of the module** — what must exist before what — and
|
|
**the bundle is a property of the mesh's own instance.**
|
|
|
|
This is also legal under the dependency rule, which is worth checking rather than assuming: a
|
|
tier-4 workload that requires a database depends on tier 1, which points *downward*. The
|
|
inverse — substrate reaching into the catalogue for a module — would not be, and is the shape
|
|
the naive "twice" answer would have created.
|
|
|
|
The same reasoning places the rest of the substrate: the bus, the object store, the image
|
|
registry. Each fails step 2, each lives once, each is pinned. That is the whole substrate: four
|
|
services, and the deliberate absence of a fifth.
|
|
|
|
**The identity provider was raised as a boundary case and is settled: it is not substrate.**
|
|
The mesh does not require one — tier 2 authenticates its own callers natively, and an identity
|
|
provider is a service the mesh hosts like any other. The substrate is four services, not five.
|
|
|
|
The test earned its keep here by turning a vague unease into one answerable question — *does the
|
|
control plane delegate authentication?* — rather than a debate about how important identity
|
|
feels.
|
|
|
|
## Becoming self-hosting — the forge and the registries
|
|
|
|
Self-improvement means the mesh hosts the things it improves itself with: a forge, an image
|
|
registry, a package registry. The obvious worry is that these duplicate — a `mesh-gitea`
|
|
for the mesh and a `gitea` for everyone else. **They do not, and the reason is worth stating
|
|
carefully, because it is the same reason the bootstrap keeps failing today.**
|
|
|
|
### They are not substrate
|
|
|
|
Run the test with the sharpened verb. *Can the control plane start without a forge?* **Yes.** It
|
|
comes up, holds inventory, answers questions and manages nodes with the modules it already has.
|
|
What it cannot do is **change itself**. That is a capability, not a precondition.
|
|
|
|
So: forge, image registry and package registry are **tier 4 workloads**. One module each, in the
|
|
catalogue, exactly like the media server. There is no mesh-specific copy.
|
|
|
|
This is not a technicality. It buys a property worth having: **if the forge dies, the mesh keeps
|
|
running.** Nodes stay managed, services stay up, only self-modification stops. Putting the forge
|
|
in the substrate would make losing it fatal, for no gain.
|
|
|
|
### But delivery needs them — is that not an upward dependency?
|
|
|
|
It would be, stated naively, and that would break the one rule the whole skeleton rests on.
|
|
|
|
It is resolved the way the constitution already says to resolve it — **depend on abstractions,
|
|
not on concrete dependencies**. Tier 2's delivery context does not require *the forge module*.
|
|
It declares requirements:
|
|
|
|
| Delivery requires | Satisfied by |
|
|
|---|---|
|
|
| a source of record for module code | whichever module provides it |
|
|
| somewhere to publish images | whichever module provides it |
|
|
| somewhere to publish packages | whichever module provides it |
|
|
| somewhere to put build artifacts | the substrate's object store |
|
|
|
|
Tier 2 defines the requirement; tier 4 provides the implementation; the binding is data. The
|
|
dependency points **downward from the provider to the interface**, which is legal, and the
|
|
control plane never names a concrete module.
|
|
|
|
The mechanism for this already exists and is the mesh's most valuable one: **provisioning**. A
|
|
module declares what it provides; a consumer declares what it requires; the mesh binds them.
|
|
The only new idea is that **the control plane is itself a consumer** — it has requirements, and
|
|
they are satisfied the same way a workload's are.
|
|
|
|
That generalisation is significant enough to need its own study, and is not settled here.
|
|
|
|
### Self-hosting is a state the mesh reaches, not a precondition
|
|
|
|
This is the part today's mesh gets wrong, and it explains a recurring class of pain.
|
|
|
|
A first node comes up from **pinned external artifacts** — upstream images, by digest, carried
|
|
in the bundle. It has to: the mesh's own registry does not exist yet, and cannot. The mesh at
|
|
this point is running and manages nodes, and is not yet self-hosting.
|
|
|
|
Self-hosting is then **reached**: the forge is installed as an ordinary workload, the mesh's own
|
|
source moves into it, the registries come up, and delivery's requirements are re-bound from
|
|
external providers to internal ones. From that point the mesh builds and deploys itself.
|
|
|
|
Stated as a lifecycle:
|
|
|
|
```
|
|
pinned external artifacts ─► mesh runs, manages nodes
|
|
│
|
|
│ forge + registries installed as workloads
|
|
│ delivery's requirements re-bound
|
|
▼
|
|
mesh builds and deploys itself
|
|
```
|
|
|
|
**Today's mesh assumes the second state from the first moment.** Its source, its packages and
|
|
its images are all expected to be self-hosted before there is anything to host them — which is
|
|
why raising a first node needs a script that exists solely to paper over the impossibility, and
|
|
why that script is the least-exercised path in the system.
|
|
|
|
Making the transition explicit has a second benefit: it is reversible. A mesh whose forge is
|
|
broken can re-bind delivery to external providers and keep improving itself while it repairs
|
|
the forge. Today that escape hatch does not exist, because the dependency is not expressed
|
|
anywhere it could be changed.
|
|
|
|
### So, concretely
|
|
|
|
One `gitea` module. One image-registry module. One package-registry module. Each a tier-4
|
|
workload. The mesh's own instances are distinguished from any other instance **by what they are
|
|
bound to, not by being different modules** — precisely the same answer as postgres, arrived at
|
|
by the same test.
|
|
|
|
## The five fates of a module
|
|
|
|
Research 005 asked whether a catalogue module should be **grouped into a domain** or **leave
|
|
the repository**. Working the tier test across the catalogue surfaces a third answer that
|
|
neither option covers, and it is the most common one.
|
|
|
|
| Fate | Means | Examples from today |
|
|
|---|---|---|
|
|
| **Absorbed into the host** | It is not a module at all. It is part of what "managing a machine" means, and belongs in tier 0. | overlay membership, packet filtering, package management, service supervision, container runtime, filesystem management |
|
|
| **Substrate** | The control plane cannot exist without it. Pinned, host-applied. | relational store, bus, object store, image registry |
|
|
| **Control-plane context** | It decides something across nodes. | connectivity policy, inventory, delivery, provisioning, observability |
|
|
| **Workload module** | The mesh hosts it. Grouped per [ADR 0017](../../02-DECISIONS/0017-modules-outside-the-core-are-grouped-by-domain.md). | media library, desktop session, collaboration tooling |
|
|
| **Leaves the repository** | A standalone application, per [ADR 0010](../../02-DECISIONS/0010-applications-live-in-their-own-repository.md). | the applications identified in research 005 |
|
|
|
|
**The first fate is the finding.** Research 005 measured the reachability cluster — proxy,
|
|
resolver, firewall, overlay — as the only place in the catalogue where modules genuinely change
|
|
together under one intent. The skeleton explains *why*: they are not four modules that ought to
|
|
be one domain module. They are four facets of one thing the host should own, currently
|
|
expressed as modules because a module was the only unit available.
|
|
|
|
Under this skeleton the overlay module and the firewall module **stop existing**. The host holds
|
|
membership and applies filtering; tier 2 decides the policy; the swappable backends stay
|
|
modules. That is a different and better answer than grouping them, and it was not visible from
|
|
inside the current frame.
|
|
|
|
It also partly answers research 005's other open question — the fifty modules that co-change
|
|
with nothing. Several are host concerns rather than domains: package management, container
|
|
runtime, filesystem tooling. Silence was the right signal after all; the wrong conclusion was
|
|
that grouping was the only available fix.
|
|
|
|
## What a module looks like on disk
|
|
|
|
Per [`skeleton.md`](skeleton.md) Move 4, a module declares **parts** — independently selectable
|
|
pieces of desired state — and produces **artifacts** — things built once per version.
|
|
|
|
```
|
|
<module>/
|
|
module.yml identity, what it provides, what it requires,
|
|
which host profiles it can land on
|
|
parts/
|
|
service/ desired state: container, volumes, exposure
|
|
provisioner/ how it grants its resource to consumers
|
|
migrations/ its own persistent state
|
|
tools/ capabilities it contributes
|
|
artifacts/
|
|
<name>/ source for something built and published
|
|
```
|
|
|
|
A module with no artifacts consumes an upstream image and builds nothing. A module with no
|
|
parts is not a module.
|
|
|
|
Worked through for the substrate's relational store:
|
|
|
|
```
|
|
mesh-substrate/store/postgres/
|
|
module.yml provides: database · profiles: [managed]
|
|
parts/
|
|
service/ the container, its volume, its network exposure
|
|
provisioner/ grants a database and role to a consumer
|
|
migrations/ none — it holds no state of its own
|
|
artifacts/ none — upstream image, pinned by digest in bundle.yml
|
|
```
|
|
|
|
## The tree, at file level
|
|
|
|
```
|
|
mesh-host/ TIER 0
|
|
cmd/host/
|
|
internal/
|
|
apply/ reconcile declared state
|
|
inventory/ what this machine is and can do
|
|
link/ outbound connection to the control plane
|
|
overlay/ membership: address, keys, tunnel
|
|
filter/ packet filtering from tier-2 policy
|
|
packages/ package management
|
|
services/ supervision
|
|
containers/ container runtime
|
|
store/ embedded local state
|
|
profile/ managed · user · edge
|
|
substrate.lock pinned tier-1 descriptor
|
|
|
|
mesh-substrate/ TIER 1
|
|
bundle.yml the pinned set, by digest
|
|
store/postgres/
|
|
bus/<broker>/
|
|
objects/<object-store>/
|
|
images/<registry>/
|
|
|
|
mesh-control/ TIER 2
|
|
record/ the event log contexts integrate through
|
|
inventory/ nodes · modules · assignments · versions
|
|
config/ settings · secrets · derivation
|
|
connectivity/ addresses · resolution · exposure · filtering · certificates
|
|
provisioning/ grants between modules
|
|
delivery/ source → artifact → node
|
|
observability/ health · logs · metrics
|
|
identity/ agents · humans · services · authorisation
|
|
work/ tasks · workflows · runs
|
|
knowledge/ memory · documents · retrieval
|
|
api/ the one interface surfaces speak to
|
|
|
|
mesh-surfaces/ TIER 3
|
|
tools/ web/ cli/
|
|
|
|
mesh-catalog/ TIER 4
|
|
<domain>/<module>/ layout as above
|
|
|
|
mesh-lab/ mesh-sdk/ hq/ (company-scoped — ADR 0028)
|
|
```
|
|
|
|
## What this does not settle
|
|
|
|
- **The identity boundary case.** Four substrate services or five.
|
|
- **Where the record lives.** Still the open question from
|
|
[`00-overview.md`](00-overview.md), and the tier test does not resolve it: the record is
|
|
needed by tier 2 and is *of* tier 2, which is exactly the shape that produces a circularity.
|
|
- **Whether absorbing into the host makes the host too large.** Six internal concerns is already
|
|
a lot for a binary whose whole argument is that it has no dependencies. The counter-argument
|
|
is that each is small and none can be optional — but this is the skeleton's biggest unproven
|
|
claim, and it should be tested by writing the host's interface before anything else.
|
|
- **The migration.** Nothing here says how today becomes this.
|