Documentation

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_token gates writes and volatile gates 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.

1st: hosted layer

2nd: cached layer

GET simple/utils/

virtual root/pypi

your uploads
utils-2.0 ✓

pypi.org cache
utils-9.9 ✗ shadowed

1st: hosted layer

2nd: cached layer

GET simple/utils/

virtual root/pypi

your uploads
utils-2.0 ✓

pypi.org cache
utils-9.9 ✗ shadowed

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 volatile hosted 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 (yanked or hidden) on the virtual index's hosted layer. The cached index's own route is untouched, the override applies wherever that hosted layer serves, and restore clears 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

On this page