diff --git a/02-DECISIONS/0075-two-stores-and-which-provides-what.md b/02-DECISIONS/0075-two-stores-and-which-provides-what.md new file mode 100644 index 0000000..af6a607 --- /dev/null +++ b/02-DECISIONS/0075-two-stores-and-which-provides-what.md @@ -0,0 +1,116 @@ +--- +topic: the tiers +status: proposed +date: 2026-09-15 +deciders: jochen +reconstructed: false +extends: 0014-no-npm-workspace.md +--- + +# 75. An artifact store is a provision; a package registry is a different one + +## Context + +Two questions have been circling, and they turn out to be one question asked twice. + +**"Should gitea be the mesh's registry?"** It serves OCI images and a dozen package ecosystems, it +is already needed — genesis clones from one — and the mesh's own registry has neither +authentication ([issue 042](../04-ISSUES/042-nothing-gives-a-node-an-account-for-a-registry/00-report.md)) +nor a transport a runtime will accept over a network +([issue 048](../04-ISSUES/048-nothing-makes-a-machine-trust-the-mesh-registry/00-report.md)). Gitea +has both. + +**"Where does the SDK come from?"** [ADR 0014](0014-no-npm-workspace.md) already answers it — each +module consumes its dependencies, the mesh's own shared library included, *from the private +registry* — and nothing installs one, so today it comes from a git URL, which is +[issue 053](../04-ISSUES/053-the-sdk-is-pinned-twice-and-the-two-disagree/00-report.md). + +**The framing that dissolves both:** `artifact-store` is already a provision, and `registry` +already provides it. So "should gitea be the registry" is not a question about replacing a +component. It is a question about **a second provider of an existing provision** — which this mesh +has a mechanism for, and uses for certificate authorities and VPNs already. + +## Decision + +**Two provisions, because they are two jobs.** + +| provision | is | for | +|---|---|---| +| `artifact-store` | content-addressed blobs, pinned by digest, no versions, no ranges | what the **mesh** delivers to **machines** | +| `package-registry` | an ecosystem's own registry — npm, cargo, PyPI, Go | what **code** resolves when it is compiled | + +They are not the same store with different clients. One is addressed by digest and immutable by +construction; the other is addressed by name and version, and resolves ranges. Conflating them is +how a mesh that pins everything ends up rebuilding one commit into two different things. + +**`registry` remains the provider genesis installs.** Not because it is better, but because of what +it is: a directory and one container, no database, no control plane, installable at step 8 of an +install where neither exists yet. Gitea needs a store and provisioning, which means a control plane, +which means the pivot has already happened — and the pivot needs somewhere to publish to. + +**Gitea also provides `artifact-store`, and a mesh may choose it.** Two providers of one provision +is a thing the mesh understands: it refuses, names both, and choosing is assigning the one you want. +A mesh that assigns gitea gets authentication and TLS for its artifacts — which is to say, **issues +042 and 048 are answered by choosing a provider that already solved them**, rather than by +reimplementing accounts and certificates in a registry that has none. + +**Gitea provides `package-registry`.** That is ADR 0014's private registry, and it is one service +rather than one per ecosystem. `verdaccio` may provide it too, for npm alone, and is then a choice +somebody makes rather than the answer. + +**The registry is not removed at the end of installing.** A mesh that never runs gitea still has an +artifact store. Retiring it is a migration a mesh performs, not a step an installation ends with. + +## Why not simply gitea, from the start + +Because genesis would need a control plane before the thing that stores the control plane's image, +and that is circular rather than merely awkward. It would also make one of the three things the +build loop cannot produce for itself into a stateful application with a database — the pivot is the +hardest part of this design already. + +And it puts every artifact in the service that is also the trust anchor for everything the mesh will +ever run ([ADR 0071](0071-where-genesis-gets-its-source.md)), which records that forge serving a +cryptominer with tampered git operations. Two blast radii are better than one. + +## Moving from one provider to the other is a designed act + +**Not a removal.** Every image a machine runs is pinned to a digest at a named store, the control +plane's own included. Changing the provider means: + +1. gitea installed, reachable, and holding an account the builder may publish with +2. every artifact mirrored +3. every declaration re-pinned, the control plane's **last**, because it is what performs the others +4. **every machine verified to have converged and to be able to pull from the new store** +5. only then the old provider unassigned, and its volume kept ([ADR 0030](0030-data-outlives-the-mesh-that-declared-it.md)) + +Step 4 is the one that is easy to skip and the only thing between this and a mesh that cannot +restart its own control plane. A machine that reboots mid-migration pulls from a store that no +longer exists, and a local image cache hides that until exactly the moment it matters. + +## Consequences + +**The bootstrap is unchanged**, which is the point of keeping the small provider. + +**042 and 048 gain a second answer.** They can be fixed in the registry, or dissolved by choosing a +provider that already has accounts and TLS. The second is less work and more service. + +**ADR 0014 becomes satisfiable.** There is a provision for the private registry, something that +provides it, and a module may depend on it — so the SDK can be published and consumed rather than +cloned, and issue 053 has somewhere to go. + +**A mesh can be minimal or complete, and both are legitimate.** One with the small registry and no +gitea builds and runs modules and cannot serve packages. That is a real configuration, not a broken +one — the same way a partial host is real. + +**And the bootstrap still has no package registry.** The first build of the shared base happens +before anything has installed one. That is the same pivot as everything else and it is **not solved +here**: it is named, so the next person does not discover it. + +## How this is checked + +| Rule | Checked by | +|---|---| +| Genesis needs no database | The installer raises a mesh of one on a machine with nothing, and the artifact store it installs has no store of its own. | +| Two providers are a choice, not a conflict | A mesh holding both is asked to resolve `artifact-store` and refuses, naming both, until one is assigned. | +| The two stores are not interchangeable | A module depending on `package-registry` is not satisfied by `artifact-store`, and the refusal says why. | +| A migration is verified before it is finished | The old provider cannot be unassigned while any machine's declaration still names it. | diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 362477f..12367d3 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -107,6 +107,7 @@ python3 00-META/checks/index.py fail if stale - **0072** — [Two graphs, and a build chain that orders itself](0072-two-graphs-and-the-build-chain.md) - **0073** — [The installer carries a builder, and the registry stays where it is](0073-the-installer-carries-a-builder.md) - **0074** — [The mesh defines a module protocol; an SDK is an implementation of it](0074-the-wire-is-specified-not-the-types.md) +- **0075** — [An artifact store is a provision; a package registry is a different one](0075-two-stores-and-which-provides-what.md) *(proposed)* ### What runs on them, and how it gets there