Design history
For someone asking why it is like this.
Every substantial change to BatleHub — a user-facing surface, a cross-crate refactor, a security-relevant default, anything expensive to undo — is argued out in an RFC before it is built. These are those documents, published unedited.
They are candid about defects that were live in shipped versions, and that is deliberate. For a self-hosted infrastructure product, the useful thing to know is not that a project has never been wrong; it is what it does when it finds out.
Every page here opens with a status banner, and the status is not uniform. The banner is generated from each document's own Status field rather than written on the page, so it cannot drift from the table it is quoting — an RFC that describes a proposal, published under a label saying it shipped, would be a claim about the product that is not true. The count and the table below come from those same rows, and from each document's own Short and Settles.
Of the 21 below, fourteen are implemented, one is accepted, one is in review, four are drafts and one is superseded.
| RFC | Status | What it settles |
|---|---|---|
| 0001 — Subdomain routing | Implemented | Reaching a registry by host name instead of by path |
| 0002 — Vulnerability flags | In review | What BatleHub knows about a package's CVEs, and who it tells |
| 0003 — UI rework | Implemented | The design system the console and this site both wear |
| 0004 — Admin composition | Implemented | The API the console was missing |
| 0004-bis — What 0004 left | Implemented | The parts 0004 did not finish, and why they were not visible |
| 0005 — One documentation tree | Implemented | Merging the two documentation trees, and putting the design system on the result |
| 0005-bis — Two readers, one home each | Implemented | Splitting the guide by audience, giving every instruction one home, cutting each page down to one subject, and turning the showcase back into an introduction |
| 0006 — A block every ecosystem can see | Implemented | Hiding blocked versions from every registry's listings, not just npm's, and stating which protocols cannot be filtered |
| 0007 — The README, per version | Implemented | Storing each version's own README, rendering it safely on the server, and making the package page answer — versions and documentation — for packages this instance holds nothing of |
| 0007-bis — The three 0007 deferred | Implemented | Rendering a README's images without telling their host who is reading, searching what a package says rather than what it is called, and asking for a version the page has told you exists |
| 0008 — mise in an air-gapped estate | Draft | Making mise install work with no route off the site: mise.lock as the bill of materials, a server that will not dial out, and verification moved to the connected side |
| 0009 — Every endpoint the client actually calls | Implemented | Serving the paths each package manager really requests, and two mechanisms so the next invented endpoint fails the build |
| 0010 — The toolchain layer | Accepted | Proxying the JDK and the Node runtime themselves, not only what they build: SDKMAN and the nodejs.org/dist tree as registry kinds, and making a blocked toolchain a refusal rather than a claim |
| 0011 — Authenticated OpenVSX access | Draft | Giving an editor that has no credential hook a way to send one: a contract file that may point at a secret rather than hold it, the pod's own Kubernetes identity, a loopback proxy for editors we do not build, and a sign-in entry in the Extensions view instead of a blank one |
| 0011-bis — Namespace-scoped visibility | Superseded by 0015 | Making a team's packages visible to that team and to the groups it grants read to: a namespace separator per ecosystem, reader groups with a per-package override, and groups on a PAT |
| 0012 — Signed URLs for the credential-less request | Implemented | Letting a client that sends no credential — Terraform's provider archive — download from a registry that is closed to everyone else |
| 0013 — What the console owes a reader | Implemented | Eleven things the package pages knew and could not act on — a search that survives a click, a version that is a link, a README you can read as source, the hosts an image may come from, and two lists that page on the operator's numbers rather than on a literal |
| 0014 — Upstream disappearance | Draft | How a package that vanished upstream is detected, held, and reported to the admin |
| 0015 — Grants on the hierarchy | Implemented | How a request is authorized: one permission vocabulary with write verbs, grants that attach to a registry/namespace/package hierarchy, and namespaces that carry visibility, immutability and gate policy for everything beneath them |
| 0016 — Retention and tombstones | Implemented | What happens to a locally published version over time: reclaiming what nobody is using, and a coordinate that can never be occupied twice |
| 0017 — Grants editor | Draft | Who writes a package- or version-tier grant, and what §4.4 filters once they exist |
0007 and 0008 were deferred behind 0009, and the reason was worth stating plainly: 0009 found six protocol defects that had all shipped green, and the common cause was tests written from our implementation rather than from what the client sends. Building further on that foundation before fixing it would put more surface on a floor we know does not hold. 0008's air-gapped case also depended directly on 0009's checksum-database and upstream-caching work.
0007-bis picks up the three open questions 0007 recommended a decision on and did not take — the image proxy, prose search, and a way to fetch a version the page has just told you exists. All three are the same shape: the page knows something it cannot act on. It has since landed, and it is the one in this set whose open questions were settled by measuring rather than by argument: two of the five were resolved against the recommendation it was drafted with, because 67 % of the images in real READMEs turned out to be SVG and because simple full-text search cannot find a README that says retrying when a reader types retry.
0009 has since landed, and with it that dependency: the Go checksum database is proxied and cached, so a second build resolves with no route off the site, and Terraform's checksum files no longer send an otherwise air-gapped provider install to the internet at its last step. 0007 has since landed on that foundation — reusing 0009's three rungs for its discovery read rather than inventing a second cache policy — and 0008 is waiting on scheduling rather than on 0009; see its own §2.7 for what that leaves.
0010 is the first to argue with what the other RFCs assumed rather than with what they built. 0006 made a block visible in every ecosystem's listings, and 0009 made every endpoint the one the client really calls — for the dependency layer. The layer underneath it, the JDK and the Node runtime a project builds on, is a row in the console that nothing enforces. Its two halves fail differently, which is why they are argued together: SDKMAN has never reached a BatleHub instance at all, while nodejs.org/dist has been mirrored as a generic registry all along — caching every byte and enforcing nothing, because a path-addressed mirror has no version to block. The cache working is what made that one invisible. It is also the rest of the estate 0008 described: mise covers some toolchains, SDKMAN and nvm cover the others, and an air gap has to hold for all of them.
They read in order: each one argues with the state the previous one left.
What is next
The table above says where each document stands. This says which order the unbuilt ones are being taken in, and — more usefully to argue with — on what criterion. It then does the same for the roadmap, which is the canonical list of what is unbuilt and is longer than this one: an RFC is written for work that is expensive to undo, and plenty of remaining work is not.
The criterion is that closing a hole that is open right now beats adding a capability that is missing. Both matter, and the second is what a release gets announced on. But a missing capability is a thing an operator does not have; an open hole is a thing they have already deployed, usually because this project's own documentation told them to configure it that way. Ordering by that turns out to group the remaining work more sharply than ordering by size or by theme would.
It also puts something at the top that is not an RFC at all. The list below was last rewritten when 0012, 0015 and 0016 were still proposals; all three have landed, and the first item is what landing 0015 left behind.
- Groups on a PAT. The one third of 0011-bis that 0015 absorbed by requirement and did not build.
UserTokencarries no groups andUserTokenAuthProviderresolves every token togroups: vec![], so agroup:subject in the shipped grant hierarchy never matches a personal access token and every piece of token-driven automation reads as an authenticated user with no groups. 0015 states the rule as settled and wrotepat_is_within_ownerto enforce it, so the invariant check is already there, comparing against an empty set. Half a day, and it is a hole in something already deployed rather than a proposal — which is the criterion above, applied to the one item nobody scheduled because no open RFC owns it. - 0010 — the toolchain layer. Accepted, no open questions, and top of the roadmap's own list.
nodejs.org/disthas been mirrored as agenericregistry all along, andgeneric_getaddresses the whole tree as one synthetic package — every byte cached, no version to block, nothing in explore, no per-version statistics. A working cache is what has kept that invisible, which is what puts it above the larger document below it: this is a hole that looks closed, not a capability that is missing. SDKMAN's broker compounds it, answering302to four different third-party CDNs, so an estate that has closed egress everywhere else finds out when it provisions a build agent.nodedistships at phase 4 andsdkmanat phase 7, each behind its own heavy suite and not before. - 0014 phases 1–3 and 5 — detection and the eviction hold. Phase 5 is the only work in the set that prevents data loss rather than closing a policy gap:
EvictionService::run_ttlselects oncached_atand knows nothing about upstream, so an artifact upstream has deleted ages out on exactly the schedule of one it still has — the cache discards the last copy in the estate precisely because nothing is refreshing it. Phases 4 and 6–9 can wait; phases 1–3 cannot, because there is nothing to exempt from eviction until something confirms a disappearance. - 0002 — vulnerability flags and exposure. The largest build here at eleven phases, which is the argument for starting it early rather than for deferring it. It is the one whose absence is felt at the worst moment: when a compromise is announced, "who already pulled this" is the first question asked and today nothing can answer it. Phase 1 — version schemes — is self-contained, reusable well beyond flags, and the phase most likely to surface a surprise, so it is worth starting alongside 0010 rather than after it.
- 0008 — the air gap. After 0010, for the reason stated above. Its four open questions are all decidable today and none needs new measurement; closing them before it is scheduled is cheaper than closing them during.
- 0011 — authenticated OpenVSX. Last: the most open questions, the widest surface — a CLI, a contract file, an editor patch, a loopback proxy and an extension — and the only one whose work extends outside this repository. Its phases 1 and 2 are the exception and do not belong at the back with the rest: server-side Bearer and Kubernetes credential work, testable with
curland a mounted token, useful with no editor involved.
The roadmap's remaining entries, against that list
ROADMAP.md carries nineteen unchecked entries against these six documents. They are not a second plan. Six of them are the list above under a different name, and belong to whichever item owns them:
| Roadmap entry | Where it sits |
|---|---|
Node distributions (nodedist) · JVM toolchains (SDKMAN) | Item 2. They are RFC 0010, split into its two halves |
| Third-party vulnerability flags · Vulnerability exposure reporting | Item 4. They are RFC 0002's two capabilities, and it is one RFC because they are one model |
| Groups on a personal access token | Item 1 |
| Instance-to-instance transfer for air-gapped estates | Item 5, one layer up. RFC 0008 phase 4 builds a signed bundle with an import path and a defined interaction with dedup; this is the same format carrying approved artifacts rather than a toolchain plan, and it should reuse it rather than invent a second one |
One entry reads as remaining and is not. Web console redesign names three things as unbuilt — /packages and /explore duplicating the catalogue, two detail URL shapes, and "no localisation exists today, every string is hardcoded English". RFC 0003 landed all nine of its phases: /explore is a redirect to /packages in ui/src/router/index.ts, and ui/src/locales/ holds en.json and fr.json. The entry is stale, not open.
The remaining twelve have no RFC, and most should not need one — they are the shapes this codebase already has, one more time:
- Two adapters — the generic mirror in
local/hybridmode, and Helm charts.docs/contributing/adding-a-registry.mdis the whole design. - Four verification rules — Sigstore/npm provenance,
cargo verify-project, YARA, external antivirus. Each is a rule besidecve_gateandlicense_gate, and RFC 0002 phase 5 is about to touch that evaluation path anyway. - Three external-source feeds — dynamic blocking rules, dynamic allowlists, IP reputation. One shape, three entries: a signed remote source merged into local policy, with the signature checked before it applies. That one is worth an RFC covering all three, because three ad-hoc fetchers each deciding what to trust is the defect RFC 0015 opens by describing.
- Three of RFC 0004-bis's own §13 — storage-backend migration (§13.3), seeding from an incumbent (§13.4), and the bundle above (§13.2). They have argued homes already; they are waiting on scheduling.
- Per-host TLS and broader fuzz targets — the first needs a reason before it needs a design, the second is standing hygiene.
One of the twelve meets the criterion this page opens with, and it is not obvious from the list: storage-backend migration. An operator who edits [storage] from filesystem to S3 strands every artifact already written — the bytes stay reachable only while the old backend is still configured, and nothing says so. The full resumable, dedup-aware migrate operation is the fix and is not small. A config warning is, and the machinery is already there: AppConfig::warnings() with a stable code, the same mechanism that raises license-gate.sbom-disabled. Worth taking ahead of its own entry.
Three things do not follow from that list and are worth stating separately.
0016 left a piece unbuilt and it is now unblocked. Retention's namespace and package tiers were waiting on the namespace blocks and the policy table, both of which shipped with 0015. NamespaceConfig refuses a retention key outright rather than ignoring one, so the gap is honest — but it is a gap, and it is small.
Two orderings above are constraints rather than preferences. 0010 precedes 0008 for the toolchain-coverage reason already argued further up this page. And 0002 precedes the open question 0014 raises about blocking a freed package name against re-registration, because closing that needs a name-level block row — a change to the block model 0002 owns, not one 0014 should invent beside it.
One decision is owed before 0014 phase 2, not during it. Its fourth open question — what outage_ratio means on a registry with three cached packages, where one missing package is 33 % and voids every sweep — got sharper when on_confirmed = "block" entered the document. On a small registry the count and age floors would be the only thing standing between one upstream hiccup and a blocked package.
What would reorder the list is a different answer to the criterion in its second paragraph. Read as what should the next release be able to do that it cannot today, 0002 is the headline and 0010 is plumbing: they swap, and 0008 follows 0010 to the front as the pair it has always been.
The RFC template itself is not published — it is a form you copy, not a document you read. It lives in the repository at docs/internal/0000-rfc-template.md.