Files
hq/02-DECISIONS/0075-two-stores-and-which-provides-what.md
T
jschoubben 1e8273eb19 Correct 0075: co-residence is not the exclusivity that matters
The first draft implied gitea and the registry could not share a machine, because
registry claims the-artifact-store at node scope and I carried that across to
gitea without asking what the claim is for.

A machine running gitea for git and packages alongside a registry serving
artifacts is an ordinary arrangement. They are different ports doing different
jobs, and nothing about one being the mesh's artifact store requires the other
not to exist.

The exclusivity that matters is mesh-wide and already expressed: provides at mesh
scope means two providers are two answers, and the resolver refuses until one is
assigned. Forbidding co-residence adds nothing and forbids something reasonable.

Whether registry should still hold that claim is left open rather than answered
from outside its manifest — it may be protecting something about its port or its
data directory that nobody wrote down.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 19:35:05 +02:00

7.5 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
the tiers proposed 2026-09-15 jochen false 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) nor a transport a runtime will accept over a network (issue 048). Gitea has both.

"Where does the SDK come from?" ADR 0014 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.

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.

And it does not claim the-artifact-store. That claim is node-scoped, so a module holding it cannot share a machine with another that does — and a machine running gitea for git and packages alongside a registry serving artifacts is an ordinary arrangement, not a conflict. They are different ports doing different jobs.

The exclusivity that matters is mesh-wide and is already expressed: provides at mesh scope means two providers are two answers, and the resolver refuses until one is assigned. Forbidding co-residence adds nothing to that and forbids something reasonable. Whether registry should still hold that claim is left open here — it may be protecting something about the port or the data directory that is not written down, and removing a claim is not a thing to do from the outside of a manifest. 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), 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)

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.
Providers may share a machine A node is assigned both gitea and a registry, and both run — only one of them answers artifact-store.
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.