Skip to content

Package formats

Repod hosts seven package formats behind one backend, one RBAC model, one audit trail, and one security pipeline. This page explains how the different formats relate to each other and to the OS-package (REPO_FORMAT) toggle described in Architecture.

Two independent groups

OS packages.deb, .rpm, .apk — are selected by the REPO_FORMAT environment variable at startup. An instance serves exactly the OS-package formats you configure it for, using the native repository tooling for each (reprepro, createrepo_c, apk index).

Build-tool packages — Maven, PyPI, npm — and the OCI container registry run alongside whichever REPO_FORMAT is active, independently of it. They're always available; there's no environment variable to enable or disable them.

Format Native client Repositories/namespaces
APT apt install Fixed set of distribution codenames
RPM dnf install / zypper install Fixed set of distribution codenames
APK apk add Fixed set of distribution codenames
Maven mvn deploy / mvn dependency:get Created on first successful deploy
PyPI pip install / twine upload Created on first successful upload
npm npm publish / npm install Created on first successful publish
OCI (Docker) docker push / docker pull Created on first successful push

The OS-package formats use a fixed, admin-managed list of distributions (e.g. jammy, almalinux9). The four newer formats use a dynamic repository model instead — closer to how Docker Hub or a private npm registry works: the first successful publish to a repository name creates it, no pre-registration required. Access to a specific repository can still be restricted afterwards (see Roles & permissions).

How each dynamic format maps onto its own repository

Maven resolves the standard GAV (group:artifact:version) coordinate path — {groupId with dots as slashes}/{artifactId}/{version}/{artifactId}-{version}[-{classifier}].{extension} — against the repository, exactly as mvn deploy/gradle publish already send it. A single deploy publishes both a .pom and a .jar (or .war, .aar, …) for the same version; Repod tracks them as separate entries so neither overwrites the other. Client checksums (.sha1/.md5) and maven-metadata.xml are accepted but never trusted as-is — Repod always regenerates both from the artifact bytes it actually stored. SNAPSHOT versions are rejected at publish time — release versions only.

PyPI implements the Simple Repository API (PEP 503) for pip install and the legacy multipart upload protocol for twine upload — the same protocol upload.pypi.org itself uses, so no custom client tooling is needed. Project names are normalized per PEP 503 (lowercased, runs of -/_/. collapsed to a single -) before matching, so My_Project and my-project resolve to the same project.

npm implements the real npm registry protocol — packument GET, tarball GET, and PUT publish with the tarball attached as base64 in the request body, the same shape npm publish sends on the wire. Authentication reuses Repod's own REST API token mechanism directly (Authorization: Bearer <token>) rather than a separate login flow — paste a Repod API token into .npmrc and npm publish/npm install work immediately, no npm login step. Scoped packages (@scope/name) are supported for both publish and install.

OCI (Docker/container images) is backed by an embedded Zot registry. docker push/docker pull talk directly to Zot over the standard Docker Registry v2 protocol — Repod itself never sits in that data path, only in issuing the short-lived Bearer token Zot validates. See Container Registry for the authentication flow and deployment details.

The same pipeline, every format

Whichever format a package arrives as, it passes through the same sequence before it's servable:

Upload / push / publish received
Format validation
Antivirus scan
CVE analysis + EPSS/KEV enrichment
Policy decision — publish, or hold for review
Published — or waiting in the CISO review queue

This is deliberate: a Critical CVE in a Python wheel or a container image is exactly as dangerous as one in a .deb, and Repod treats it that way. There is no "lightweight" path for the newer formats that skips scanning — see Security Pipeline for the full breakdown.

For a container image, antivirus and CVE scanning run against the image's raw layer blobs directly (each layer is just a tar.gz, scanned the same way a .deb's ar archive or an .rpm's cpio archive already are — no manual filesystem extraction step). Grype auto-detects the language/OS ecosystem from the artifact itself for Maven, PyPI, npm, and OCI, so no per-format CVE integration was needed beyond pointing it at the right file.

Digest pinning on import: importing an image from a public registry resolves the requested tag to its manifest digest once, and every later step — scan, review decision, publish — operates on that exact, immutable digest, never on the tag again. A tag can be repointed at different content later; the review decision your team recorded never silently follows it.

Dependency CVE scanning (Maven, PyPI, npm)

Grype scans an artifact's own bytes — the compiled .jar, the wheel, the tarball. It has no visibility into that artifact's declared dependencies, which get resolved separately at a consumer's build time (npm install, mvn/gradle resolving a version range, pip install). A vulnerable dependency named in package.json, a .pom's <dependencies> block, or a wheel/sdist's requires_dist metadata was therefore invisible to Repod's CVE pipeline.

A second, independent check closes that gap: Repod extracts the declared direct dependencies (no transitive resolution — a full dependency-tree walk would need either a real semver resolver or a bundled lockfile, neither of which a published artifact carries) and queries OSV.dev by package name, per ecosystem (npm, Maven, PyPI). The lookup is by name only, not by the declared version range — deliberately over-inclusive, since narrowing by an unresolved range risks a false negative more than a wide result risks noise; a human reviewer judges whether the actually-resolved version is really affected.

A dependency finding that breaches your CVE policy routes to the same review queue as a finding in the package's own code — with one difference: a policy that would otherwise block a dependency match is capped down to review instead, never an outright rejection. Unlike a CVE in the package's own bytes, a name-only dependency match is a noisier signal (a meaningful share turn out to already be fixed upstream), so it flags for human judgment rather than blocking the publish outright. A dependency finding never downgrades or overrides a stricter verdict already reached from scanning the package's own bytes.

This coverage is populated automatically for native publish/deploy (npm publish, mvn deploy via the .pom, twine upload) and for validated import from the public registries below, since each of those already carries or fetches the dependency manifest. A raw manual upload with no accompanying dependency metadata has nothing to scan for this check.

Native protocols, not a custom API

Every format speaks the real upstream protocol its ecosystem already uses. mvn deploy, pip install / twine upload, npm publish, and docker push / docker pull all work against Repod exactly as they would against Maven Central, PyPI, npmjs.org, or Docker Hub — pointed at your Repod URL instead, with your Repod credentials. There's nothing to install on the client beyond the tool your team already uses.

See Client Setup for APT/RPM/Maven/PyPI/npm configuration and Container Registry for Docker.

Importing from public sources

Rather than requiring every artifact to be built and pushed manually, three of the four dynamic formats also support validated import: point Repod at a package on Maven Central, PyPI, npmjs.org, or a public Docker Hub image, and it downloads, scans, and publishes (or queues for review) that exact artifact — without ever bypassing the security pipeline. This is the same mechanism already used to mirror upstream security advisories for OS packages, extended to the newer formats.

Each of Maven/PyPI/npm/OCI can reach Repod's storage three ways, and all three run through the identical validation pipeline described above: the format's own native publish/deploy protocol (mvn deploy, twine upload, npm publish, docker push), a manual authenticated upload for an artifact you already have on disk, or validated import from the format's public registry. No path skips scanning to save time — the only difference between them is where the bytes come from.