The index model
An index server earns its keep the day you have a package that must not come from the public internet. This page explains velodex's answer. An index is the list of packages a client installs from (pip and uv call it an index-url); a registry is the same idea under a different name. velodex builds every index from two independent choices — a role (what the index does) and an ecosystem (which packaging format it speaks) — and one composition rule, and it is that rule that is a security control before it is a convenience.
Two axes: role and ecosystem
Every index velodex serves is a triple: a role, an ecosystem, and a key (its name). The two axes are independent.
- role is what the index does. There are three:
- cached — a read-through cache of one upstream index. "Upstream" means the index velodex fetches from, e.g. pypi.org. On a first request velodex fetches from upstream, stores the result, and serves it; later requests come from local disk. (This was called a "mirror".)
- hosted — an authoritative store you upload to. Nothing upstream; the packages live here because you published them. (This was called a "local" index.)
- virtual — an ordered aggregation of other indexes served under one URL. Its member list is called
layers. (This was called an "overlay".)
- ecosystem is which packaging format the index speaks: pypi today, with OCI and npm planned. It fixes the wire protocol (for PyPI, the PEP 503/691 Simple API) and the artifact shapes (wheels and sdists). See ecosystems.
The axes are orthogonal at creation but coupled at aggregation: a virtual index may only combine members of the same ecosystem. Everything below describes the roles for the PyPI ecosystem, the only one shipping today; the same shapes apply unchanged as ecosystems are added.
Prior art
The index servers teams run in production converged on the same role shape, and velodex adopts it:
- Artifactory aggregates local and remote repositories into a virtual repository behind one URL, with a default deployment target for writes and local-before-remote resolution.
- Nexus groups hosted and proxy repositories the same way; the member order decides who wins, and the documentation recommends hosted first.
The shared pattern: a read-through cache primitive, a writable hosted primitive, and an ordered composition served at one URL where your own content wins over upstream. velodex names these cached, hosted, and virtual.
velodex's three roles
- A cached index proxies and caches one upstream, with its own credentials.
- A hosted index stores uploads;
upload_tokengates writes andvolatilegates deletion. - A virtual index serves an ordered list of other indexes under one route. Resolution is first-match per filename; versions union. Uploads land in the virtual index's designated hosted layer. A layer can be another virtual index, which gives inheritance chains.
Filename-level (rather than project-level) shadowing means you can override one broken wheel of an upstream release while its sdist and its other wheels continue to come from the cached layer.
Why shadowing is a security control
The usual way to mix private and public packages is client-side: a private index in --extra-index-url, pypi.org as the
default. pip treats both indexes as equals and installs whichever offers the higher version. Anyone who registers your
internal package's name on pypi.org with version 99.0 now wins the race. This is
dependency confusion, the technique that
compromised PyTorch's nightly channel and, in its original
disclosure, three dozen major companies. Client-side mitigations exist (uv's explicit index pinning, for one) but must
be repeated in every project, for every tool, forever.
A virtual index moves the decision server-side. The client has one index-url and no fallback; the virtual index's
hosted layer is consulted first; a name that exists in the hosted layer never falls through to the cached layer. The
guarantee holds for pip, uv, poetry, and whatever comes next, because it lives where the indexes meet rather than in
each client's configuration. Publishing a package privately is what turns its name off upstream; there is no separate
deny-list to maintain.
Removal semantics
PyPI distinguishes hiding a release from destroying it, and velodex keeps both:
- Yank (PEP 592) marks a file so resolvers skip it while exact-pin installs still succeed. It is reversible and is the right tool for a bad release that someone may already depend on.
- Delete removes uploaded records outright and is only allowed on
volatilehosted indexes. For a virtual index this un-shadows the upstream file. The content-addressed blob stays, since another index may reference the same digest. - Upstream files cannot be modified on their cached index, so yanking or deleting one through a virtual index
records an override (
yankedorhidden) on the virtual index's hosted layer. The cached index's own route is untouched, the override applies wherever that hosted layer serves, andrestoreclears a hidden marker. This is how a broken upstream release is pulled from your resolvers within seconds, reversibly, without forking the cache.
The default topology
Out of the box velodex runs a pypi.org cached index plus an empty hosted index, combined by a virtual index at
root/pypi. One URL therefore serves the whole public index, and the day you need to host a private package you add a
token; nothing about the client setup changes.
In practice
- Build the topology: compose virtual indexes, proxy a private upstream
- Publish into it: publish; undo things: yank and delete
- The vocabulary in one place: glossary
- The wire formats underneath: standards