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:
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 publishAuthentication 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>/...:
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
apiscope. A missing/invalid credential gets a real 401 withWWW-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/PUTto stream and finalize) and content-addressed storage of both blobs and manifests, keyed by their SHA-256 digest — aHEADrequest 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/DELETEand 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:
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, soMy-Package,my_package, andmy.packageall 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 (theapiscope) 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/:
[registries.khub]
index = "sparse+https://<host>/api/organizations/<org>/packages/cargo/"cargo login --registry khub <personal-access-token>
cargo publish --registry khub- cargo sends its token in a bare
Authorizationheader with no scheme prefix at all — notBearer, notBasic— 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.cratetarball — parsed directly rather than treated as a generic file upload.cargo yank/cargo yank --undoare both supported; a yanked version stays downloadable (existingCargo.lockfiles must keep working) but is marked"yanked": truein 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:
<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, sincegroupIdcan 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
.jarplus 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/.md5checksum 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 ownyyyyMMddHHmmssformat.
NuGet-compatible registry (.NET)#
A NuGet V3 feed at /api/organizations/:orgSlug/packages/nuget/, covering the flat-container restore protocol and package push:
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.0for restore,PackagePublish/2.0.0for push). - Push authenticates via the
X-NuGet-ApiKeyheader — NuGet's own convention, distinct from every other Basic-auth-based registry on this page. - A pushed
.nupkgis a zip archive; the server opens it, reads the embedded.nuspecXML 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.configworks 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:
{
"repositories": [
{ "type": "composer", "url": "https://<host>/api/organizations/<org>/packages/composer/" }
]
}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/publishcomposer 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:
| Policy | Effect |
|---|---|
| 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.
