--- topic: the tiers status: accepted date: 2026-09-25 deciders: jochen reconstructed: false extends: 0075-two-stores-and-which-provides-what.md --- # 109. A package registry seat is one per ecosystem, not one for all of them ## Context Fixing `builder`'s consumption of `package-registry` tonight surfaced the shape ADR 0075 actually left implicit. 0075 split `artifact-store` from `package-registry` and said the second is "an ecosystem's own registry — npm, cargo, PyPI, Go" — but it defined one provision for all four, not one each. What that produces, read from the manifests as they stand: - `gitea`'s `module.json` declares `provides: package-registry` once, and its `serves` block carries exactly one path: `npm-path`. Nothing names a cargo or PyPI endpoint, though gitea's own package API serves both. - `builder` had, until tonight, a hand-written JSON fragment standing in for a real grant — `{"provision": "package-registry", "from": "gitea", "at": "127.0.0.1", ...}` — because nothing in the interface gave it a real one to ask for. The fragment named `npm-path` specifically; there was nowhere to put a second ecosystem's endpoint even if one had been wired. - The fix applied tonight declares `requires: ["artifact-store", "package-registry"]` and lets the mesh mint the grant properly — correct for what exists today, but it is one seat standing in for what should be several, the same conflation 0075 itself named and did not resolve for this provision specifically. **The version-skew problem 0075 wrote down for the artifact-store/package-registry split repeats one level down, inside "package registry" itself:** npm resolves by name and range from one namespace, cargo from another, and a single grant conflates them exactly the way one store for both digests and ranges would have. ## Considered Options **1. One `package-registry` provision, gitea answers every ecosystem it can.** What exists today. Simplest to grant — one credential, one binding, done once per consumer. Rejected: a consumer that only ever needs npm still receives a grant shaped to cover cargo and PyPI, and there is no way to hand off *only* npm to a different provider (verdaccio, say) without renegotiating the whole provision for every consumer of any ecosystem. **2. One provision, parameterised by ecosystem.** `requires: package-registry` plus a declared `ecosystem: npm` alongside it, still one interface. Rejected: the `provides`/`requires` refusal mechanism this mesh already uses (two providers of one provision is a naming conflict until resolved) would need to become conditional on a parameter it does not otherwise carry anywhere in the mesh's resolution — a special case for exactly one provision, rather than the mesh's existing mechanism applied again. **3. One provision per ecosystem — `npm-package-registry`, `cargo-package-registry`, `docker-package-registry`, and so on, each independently `provides`/`requires`.** Chosen. ## Decision **A package registry seat is one per ecosystem.** `npm-package-registry`, `cargo-package-registry`, `docker-package-registry` — each its own provision, resolved, granted, and refused exactly the way `artifact-store` and today's single `package-registry` already are. Adding an ecosystem is adding a provision, not widening one. **Gitea may hold several seats at once.** Nothing here says gitea answers only one; ADR 0075 already established that a provider may answer more than one named thing on one machine ("a mesh running gitea for git and packages alongside a registry serving artifacts is an ordinary arrangement"). Gitea fulfilling `npm-package-registry` and `cargo-package-registry` both is the expected shape, not an exception. **Each seat's grant is independent.** A consumer that only needs npm holds only the `npm-package-registry` grant. Moving that one ecosystem to a different provider — verdaccio, named directly as the motivating case — means assigning `npm-package-registry` to verdaccio and leaving every other seat exactly where it was. No consumer of `cargo-package-registry` observes the change; no manifest naming `package-registry` broadly needs to be found and re-read. **`builder`'s fix tonight is the interim shape, not the target.** It correctly consumes the one seat that exists today (`package-registry`, npm in practice). Splitting it becomes, later, replacing that one line with the ecosystems `builder` actually uses — a manifest change, not a redesign of how `builder` asks for anything. ## Consequences - `gitea`'s `module.json` gains a `provides` entry per ecosystem it actually serves, each with its own `serves` block (`npm-path`, a cargo path, a PyPI path) in place of the one `package-registry` entry with a single `npm-path` inside it. - `gitea`'s provisioner (`modules/gitea/provisioner/index.ts`) currently runs one `runProvisioner` registration for `package-registry`; each seat needs its own registration, or one provisioner keyed by which seat's `create`/`remove` fired — the mesh's `Provision` type does not yet carry which named provision a call is for when a module answers more than one, and that is worth checking before assuming the harness already supports it. - Every consumer's `requires` moves from the one name to however many ecosystems it actually uses. `builder` is the only known consumer today; widening later is one manifest line per module, not a migration. - `verdaccio`'s role sharpens: not "package-registry, an alternative for npm alone" (0075's phrasing) but a named `npm-package-registry` *provider*, a straight swap against gitea's answer to the same seat. - Not solved here: whether `cargo-package-registry` and `pypi-package-registry` are needed at all before something actually consumes them. This record names the shape; building unused seats is its own decision. ## References - [ADR 0075](0075-two-stores-and-which-provides-what.md) — the record this extends; defined `package-registry` as the second provision without splitting it per ecosystem. - `mesh-catalog modules/builder/module.json` — tonight's fix, the interim single-seat shape. - `mesh-catalog modules/gitea/module.json`, `modules/gitea/provisioner/index.ts` — today's single-provision, npm-only implementation.