DocsRegistries

Package & container registries

npm, OCI/Docker, PyPI, Cargo, Maven, NuGet, and Composer — seven ecosystems, one auth model, all self-hosted.

npm-compatible package registry#

Each organization exposes a registry endpoint at /api/organizations/:orgSlug/packages/npm/. Point npm/pnpm/yarn at it:

bash
npm config set @your-scope:registry https://<host>/api/organizations/<org>/packages/npm/
npm config set //<host>/api/organizations/<org>/packages/npm/:_authToken <personal-access-token>
npm publish

Authentication reuses personal access tokens (the api scope), sent as a standard npm registry auth token — there is no separate registry-specific credential. Published versions are immutable once published (re-publishing the same version is rejected, matching npm's own registry semantics) and are listed with their ecosystem, version count, and publisher under Organization → Packages.

Container registry (OCI Distribution)#

Each organization is also a fully standards-compliant OCI Distribution Specification v2 registry, mounted at /v2/<orgSlug>/<image>/...:

bash
docker login <host>
# username: anything; password: a personal access token with the "api" scope
docker tag myimage <host>/<org>/myimage:latest
docker push <host>/<org>/myimage:latest
docker pull <host>/<org>/myimage:latest
  • Auth is HTTP Basic (per the Docker registry auth convention), decoded server-side into a KHUB personal access token — the "username" is ignored, only the token (sent as the password) is checked, and it must carry the api scope. A missing/invalid credential gets a real 401 with WWW-Authenticate: Basic realm="KHUB Registry", so the Docker/Podman CLI's own auth-retry flow works unmodified.
  • Every response includes Docker-Distribution-Api-Version: registry/2.0, which is how client tooling detects v2-API compatibility.
  • Supports the full blob-upload session lifecycle (POST .../blobs/uploads/ to start, PATCH/PUT to stream and finalize) and content-addressed storage of both blobs and manifests, keyed by their SHA-256 digest — a HEAD request against an existing digest is used by clients to skip re-uploading layers already present, exactly as with any other OCI-compliant registry.
  • Manifest PUT/GET/HEAD/DELETE and tag listing (GET .../tags/list) round out full read/write/delete parity with Docker Hub or any other conformant registry — no KHUB-specific client plugin is required.

PyPI-compatible package registry#

Each organization also runs a PEP 503 "simple" Python package index at /api/organizations/:orgSlug/packages/pypi/simple/, plus the legacy upload endpoint twine speaks:

bash
pip config set global.index-url https://__token__:<personal-access-token>@<host>/api/organizations/<org>/packages/pypi/simple/
pip install your-package

twine upload --repository-url https://<host>/api/organizations/<org>/packages/pypi/ -u __token__ -p <personal-access-token> dist/*
  • pip needs no plugin — it's a standard PEP 503 index: a root page linking every package name, and a per-package page linking every published file with a #sha256=... fragment that pip verifies the download against.
  • Package names are normalized per PEP 503 (lowercased, runs of -/_/. collapsed to a single -) before being looked up, so My-Package, my_package, and my.package all resolve to the same package — exactly as they would on real PyPI.
  • twine's upload always authenticates with HTTP Basic — conventionally username __token__ and a personal access token (the api scope) as the password, the same convention real PyPI uses for token-based auth.
  • Current limitation: one distribution file per version. Publishing a second file (say, a wheel) for a version that already has one (say, an sdist) is rejected with a clear conflict error rather than silently accepted.

Cargo-compatible registry (Rust)#

A crates.io-compatible sparse HTTP registry (the modern protocol, not the older git-index format) at /api/organizations/:orgSlug/packages/cargo/:

~/.cargo/config.toml
[registries.khub]
index = "sparse+https://<host>/api/organizations/<org>/packages/cargo/"
bash
cargo login --registry khub <personal-access-token>
cargo publish --registry khub
  • cargo sends its token in a bare Authorization header with no scheme prefix at all — not Bearer, not Basic — which is cargo's own convention and exactly what the registry expects.
  • The index is generated on demand from the database rather than kept as static files: a package's index entry is newline-delimited JSON, one line per version, computed at request time and sharded into cargo's directory scheme.
  • cargo publish's upload is a specific binary framing — a 4-byte little-endian length, that many bytes of JSON metadata, another 4-byte length, then that many bytes of the .crate tarball — parsed directly rather than treated as a generic file upload.
  • cargo yank/cargo yank --undo are both supported; a yanked version stays downloadable (existing Cargo.lock files must keep working) but is marked "yanked": true in the index so it's never chosen for a new dependency resolution.

Maven-compatible registry (Java/Kotlin/Gradle)#

A standard Maven repository layout at /api/organizations/:orgSlug/packages/maven/maven2/, usable from either Maven or Gradle:

~/.m2/settings.xml
<server>
  <id>khub</id>
  <username>x</username>
  <password>${env.KHUB_TOKEN}</password>
</server>
  • The repository coordinate (groupId, artifactId) is recovered from the tail of whatever path was requested, since groupId can be any number of dot-separated segments — there's no fixed path depth to match against.
  • Unlike every other registry on this page, a Maven version can hold multiple files — the .jar plus its .pom, and optionally -sources.jar/-javadoc.jar — and KHUB's Maven registry supports that fully; each file making up a version is tracked individually, and re-deploying an already-published file is rejected (Maven artifacts are immutable once published) while a new file for an existing version is accepted normally.
  • .sha1/.md5 checksum sidecar requests are computed fresh from the real file's bytes on every read rather than stored.
  • maven-metadata.xml (and its checksum sidecars) is generated on demand from the set of published versions — listing <latest>, <release>, every <version>, and a <lastUpdated> timestamp in Maven's own yyyyMMddHHmmss format.

NuGet-compatible registry (.NET)#

A NuGet V3 feed at /api/organizations/:orgSlug/packages/nuget/, covering the flat-container restore protocol and package push:

bash
dotnet nuget add source https://<host>/api/organizations/<org>/packages/nuget/v3/index.json -n khub
dotnet nuget push -s khub -k <personal-access-token> your.package.nupkg
  • dotnet/nuget.exe first fetch a service index (v3/index.json) to discover every other URL, including where to push — KHUB's index only advertises the two resource types it actually implements (PackageBaseAddress/3.0.0 for restore, PackagePublish/2.0.0 for push).
  • Push authenticates via the X-NuGet-ApiKey header — NuGet's own convention, distinct from every other Basic-auth-based registry on this page.
  • A pushed .nupkg is a zip archive; the server opens it, reads the embedded .nuspec XML to learn the package's real id/version, then stores it addressable by that id (lowercased, matching NuGet's own case-insensitive id resolution).
  • Current limitation: the full search/registrations-catalog metadata (what IDEs use to browse "what versions are available" interactively) isn't implemented — restoring a specific, already-known version via PackageReference/packages.config works today.

Composer-compatible registry (PHP)#

Composer has no standard dynamic publish protocol of its own — real-world private Composer registries are almost always statically generated or Git-backed — so KHUB implements the read side as a standard Composer "composer"-type repository, and publishing as its own straightforward upload endpoint:

composer.json
{
  "repositories": [
    { "type": "composer", "url": "https://<host>/api/organizations/<org>/packages/composer/" }
  ]
}
bash
curl -u x:<personal-access-token> \
  -F name=vendor/package -F version=1.0.0 -F file=@dist.zip \
  https://<host>/api/organizations/<org>/packages/composer/publish

composer install/composer require need nothing beyond the repositories entry above — they fetch packages.json (a single inline listing) and download each package's zip directly from the dist.url it points to. Publishing is authenticated the same Basic-auth way as Maven and the container registry, since Composer doesn't define its own convention here the way npm/PyPI/cargo/NuGet each do.

Retention#

An organization can configure automatic pruning under Organization settings → Retention, applied by a background sweep, independent of the registries' normal read/write paths:

PolicyEffect
CI artifact retention (days)Deletes job artifacts older than N days.
Package version retention (count)Keeps only the N most recent versions of each published package, deleting older ones.
Untagged OCI manifest retention (days)Deletes container manifests with no tag pointing at them (the equivalent of docker image prune for the registry itself) once they're older than N days — tagged manifests are never swept regardless of age.

Leaving a policy field unset disables that particular sweep; retention is opt-in per knob, not an all-or-nothing switch.