Skip to content

Other formats

The four flagship formats (Maven, npm, PyPI, Docker / OCI) have their own pages. This page summarises the remaining 40 with endpoint shape, repository modes, and format-specific quirks.

Endpoint convention

Every format uses the same shape:

https://orbitalreg.example.com/<format>/<project-slug>/<repo-slug>/<format-specific-path>

The <format> segment is the canonical name listed below.

Support tiers

Every format below is production-usable (registered adapter, real integration-test coverage), but a handful ship with a documented, narrower feature set than their peers. Those are marked Experimental with a status: line explaining the gap; everything else is GA. This mirrors frontend/src/lib/formats.ts (the UI surfaces the same caveat as a create-repo dropdown suffix and dashboard tile tooltip) — see docs/PRODUCT-ROADMAP.md item 148 for how this classification was derived.

Language ecosystems

RubyGems

endpoint: /rubygems/<project>/<repo>/
publish:  gem push --host https://orbitalreg.example.com/rubygems/acme/internal
install:  gem source --add ...

Cargo

endpoint: /cargo/<project>/<repo>/
config:   .cargo/config.toml -> [registries.orbital] index = "..."

Go modules

endpoint: /gomod/<project>/<repo>/
env:      GOPROXY=https://orbitalreg.example.com/gomod/acme/all
          GOSUMDB=sum.golang.org  (or off / OrbitalReg-hosted sumdb)

NuGet

endpoint: /nuget/<project>/<repo>/
config:   nuget.config -> packageSources/add key="orbital" ...
status:   GA (V2 push/restore) — V3 search/autocomplete endpoints are stubbed.

Hex (Elixir)

endpoint: /hex/<project>/<repo>/
config:   mix hex.repo add orbital ... --auth-key ...

Swift

endpoint: /swift/<project>/<repo>/
config:   ~/.swiftpm/configuration/registries.json

Dart / pub

endpoint: /pub/<project>/<repo>/
env:      PUB_HOSTED_URL=https://orbitalreg.example.com/pub/acme/all

Composer (PHP)

endpoint: /composer/<project>/<repo>/
config:   composer.json -> repositories[]: { "type": "composer", "url": "..." }

CRAN (R)

endpoint: /cran/<project>/<repo>/
config:   options(repos = c(orbital = "..."))
status:   Experimental — archive browsing only (`archive.rds` stub); no live
          PACKAGES index generation yet.

Deno

endpoint: /deno/<project>/<repo>/   (deno.land/x-compatible mirror)
import:   import { x } from "https://...example.com/deno/acme/all/x/<mod>@<ver>/mod.ts"
publish:  PUT one file at a time to the same path

Nimble (Nim)

endpoint: /nimble/<project>/<repo>/
config:   nimble.ini →  [PackageList]
                        name = "orbital"
                        url = "https://.../nimble/<project>/<repo>/packages.json"
publish:  PUT /packages/<name> with the package-list entry as JSON
          ({url, method, tags, description, license, web} or {alias};
          optional registry-only `requires` array, validated against
          nimble's SemVer-range / #rev grammar)
status:   Experimental — package-list index (packages.json, ETag/304)
          only; nimble fetches package sources from each entry's
          upstream VCS URL, so dependency resolution works exactly when
          every transitive dependency has an entry here (or in the
          official list). The registry does not host package sources.

Hugging Face

endpoint: /huggingface/<project>/<repo>/
config:   HUGGINGFACE_HUB_ENDPOINT=...

OS package ecosystems

Debian / .deb

endpoint: /debian/<project>/<repo>/
config:   /etc/apt/sources.list.d/orbital.list →
          deb [signed-by=...] https://...example.com/debian/acme/all stable main

RPM

endpoint: /rpm/<project>/<repo>/
config:   /etc/yum.repos.d/orbital.repo with baseurl=...

Alpine APK

endpoint: /alpine/<project>/<repo>/
config:   /etc/apk/repositories  +  /etc/apk/keys/orbital.rsa.pub

Conda

endpoint: /conda/<project>/<repo>/
config:   ~/.condarc → channels: [...]

OPKG (OpenWrt)

endpoint: /opkg/<project>/<repo>/
config:   /etc/opkg/customfeeds.conf -> src/gz orbital ...

Per-architecture feeds. Real OpenWrt mirrors publish one feed directory per target architecture, and every repo exposes the same shape: /opkg/<project>/<repo>/<arch>/Packages[.gz|.sig] lists exactly the packages whose control Architecture equals <arch> — plus Architecture: all packages, which belong in every feed. The .ipk files resolve feed-relative, so a device config like

src/gz orbital https://registry.example.com/opkg/acme/openwrt/aarch64_cortex-a53

only ever sees (and installs) compatible packages. Uploads always go to the repo root — feed membership is derived from the control stanza, not the upload URL. The flat root feed (/Packages) keeps listing all architectures for single-arch repos and existing configs. When an opkg_packages signing key is configured, Packages.sig is served per feed and signs that feed's exact index bytes.

Nix binary cache

endpoint: /nix/<project>/<repo>/
config:   nix.conf -> substituters = https://...example.com/nix/acme/all
write:    nix copy --to https://...example.com/nix/acme/all /nix/store/<hash>-<pkg>
pubkey:   GET /nix/<project>/<repo>/nix-cache-pubkey  (the trusted-public-keys line)

Signed narinfo (Sig: lines). When the signing subsystem is enabled (set signing.enabled: true in the Helm chart, which delivers ORBITALREG_SIGNING_MASTER_KEY from a Kubernetes Secret — or set the env var yourself) and an Ed25519 key with purpose nix_narinfo exists, the cache co-signs every .narinfo it serves: it computes the canonical 1;StorePath;NarHash;NarSize;References fingerprint and appends a Sig: orbitalreg-<fp16>:<base64> line. Uploader-supplied Sig: lines (e.g. from cache.nixos.org) are preserved, so multi-signer setups keep working.

Setup, once per deployment:

sh
# 1. Mint the cache key (repo-scoped; omit repo_id for a global key)
curl -X POST https://.../api/admin/signing/keys \
  -H 'Content-Type: application/json' \
  -d '{"purpose": "nix_narinfo", "algorithm": "ed25519"}'

# 2. Fetch the trust line and add it to nix.conf on every client
curl https://.../nix/acme/all/nix-cache-pubkey
#  -> orbitalreg-<fp16>:<base64-ed25519-pubkey>
ini
# nix.conf on consuming machines
substituters = https://...example.com/nix/acme/all
trusted-public-keys = orbitalreg-<fp16>:<base64-ed25519-pubkey>

With require-sigs = true (the default for non-trusted users), Nix refuses unsigned store paths — verify end-to-end from a second host with nix store verify --store https://.../nix/acme/all /nix/store/<hash>-<pkg> or simply nix-store -r a path that only exists in the cache. Without a configured nix_narinfo key the cache serves narinfo exactly as uploaded (unsigned), the historical behaviour.

Homebrew

endpoint: /homebrew/<project>/<repo>/   (self-hosted tap)
publish:  PUT Formula/<Name>.rb | Casks/<Name>.rb | bottles/<file>.bottle.tar.gz
install:  brew install --formula https://...example.com/homebrew/acme/tools/Formula/<Name>.rb
index:    GET tap.json          (formulae + casks + bottles, with download URLs)
offline:  extract /tap.tar.gz into $(brew --repository)/Library/Taps/orbitalreg/<repo>/

Authentication uses the standard orbr_… token as the HTTP Basic password. For brew/curl on developer Macs, put it in ~/.netrc so installs work without embedding credentials in URLs:

machine orbitalreg.example.com
  login ci
  password orbr_xxxxxxxxxxxx

Publish a formula from CI (any role ≥ developer):

sh
curl -n -fST Formula/Mytool.rb \
  https://orbitalreg.example.com/homebrew/acme/tools/Formula/Mytool.rb

Metadata (desc, homepage, url, version, license, sha256) is parsed from the Ruby DSL on upload; append ?version=1.2.3 when the DSL omits an explicit version line and none is derivable from url. Upload precompiled bottles the same way — the filename carries the coordinates (<name>-<version>.<platform>.bottle[.<rev>].tar.gz):

sh
curl -n -fST mytool-1.2.3.arm64_sonoma.bottle.tar.gz \
  https://orbitalreg.example.com/homebrew/acme/tools/bottles/mytool-1.2.3.arm64_sonoma.bottle.tar.gz

tap.json indexes everything published; tap.tar.gz snapshots the whole tap (formulae + casks + a generated README with bottle URLs) for air-gapped Mac fleets — extract it into $(brew --repository)/Library/Taps/orbitalreg/<repo>/ and brew tap orbitalreg/<repo> registers it without git access. Out of scope: bottle building (your CI compiles bottles; OrbitalReg hosts the output) and git smart-HTTP tap serving — the tarball path covers the offline story.

Container + image ecosystems

Helm

endpoint: /helm/<project>/<repo>/   (also OCI via /<project>/<repo>/)
config:   helm repo add orbital https://...example.com/helm/acme/all

The classic index.yaml chart-repo path above and the OCI registry path below serve the same helm-format repo side by side — a chart pushed one way is visible only through that way's listing (the OCI path writes to the Docker-manifest store, not index.yaml), but both share the same repo, project, and auth.

OCI registry (helm push/pull oci://…)

Helm's OCI support (stable since 3.8) reuses the Docker v2 registry protocol, so a helm-format repo accepts helm push/pull/install against the same endpoint shape as Docker:

bash
echo $ORBITALREG_TOKEN | helm registry login orbitalreg.example.com \
  -u $ORBITALREG_USER --password-stdin

helm push mychart-1.2.3.tgz oci://orbitalreg.example.com/acme/internal

helm pull oci://orbitalreg.example.com/acme/internal/mychart --version 1.2.3

helm install my-release \
  oci://orbitalreg.example.com/acme/internal/mychart --version 1.2.3

The chart name and version are read from Chart.yaml, not the CLI invocation — helm push publishes to <project>/<repo>/<chart-name>:<chart-version>. If a .prov provenance file was generated (helm package --sign) it is pushed as an extra manifest layer automatically; the repo browser shows a provenance badge next to any chart version that carries one.

Ollama

endpoint: /ollama/<project>/<repo>/   (OCI-distribution-shaped model registry)
config:   OLLAMA_HOST=https://...example.com/ollama/acme/all
pull:     ollama pull <model>:<tag>

Vagrant

endpoint: /vagrant/<project>/<repo>/
config:   Vagrantfile -> config.vm.box_url = "https://.../vagrant/acme/all/acme/base"
          # or: export VAGRANT_SERVER_URL=https://.../vagrant/acme/all
          #     vagrant box add acme/base
publish:  # one-shot (self-releasing):
          curl -u user:$ORBR_TOKEN -T box.box .../vagrant/acme/all/acme/base/1.2.3/virtualbox.box
          # or the Vagrant Cloud ("Atlas") workflow under <endpoint>/api/v1/:
          #   POST /api/v1/box/acme/base/versions        {"version":{"version":"1.2.3"}}
          #   GET  /api/v1/box/acme/base/version/1.2.3/provider/virtualbox/upload -> {"upload_path"}
          #   PUT  <upload_path>  (box bytes)
          #   PUT  /api/v1/box/acme/base/version/1.2.3/release
update:   vagrant box update   # sees a version exactly when it is released

Versions carry a release lifecycle (unreleasedactiverevoked); the box-metadata JSON only lists active versions, so half-uploaded versions never leak to clients mid-publish. Direct curl -T uploads self-release (pre-existing behaviour); uploads into an unreleased Atlas-created version stay hidden until the explicit release call, and revoke pulls a version back out of the metadata. Auth on the api/v1 surface is the platform's usual HTTP Basic with an orbr_… token — not Vagrant Cloud's Bearer tokens. Versions on the Atlas path are dotted numerics (1.2.3); POST /api/v1/boxes and POST …/providers validate and echo without persisting (a box materializes with its first version, a provider with its upload).

IaC / build tools

Terraform registry

endpoint: /terraform/<project>/<repo>/  (registry v1 protocol)
config:   ~/.terraformrc -> credentials "..." { token = "..." }
mirror:   /terraform/<project>/<repo>/providers/  (provider network mirror protocol, see docs)

Argo Workflows

endpoint: /argoworkflow/<project>/<repo>/
index:    /index.yaml (Helm-style) lists WorkflowTemplate/ClusterWorkflowTemplate .tar.gz bundles
consumers: ArgoCD, `argo template lint`

Kustomize

endpoint: /kustomize/<project>/<repo>/  (OCI-distribution, KEP-2299, kustomize >=5.0)
build:    kustomize build oci://...example.com/kustomize/acme/all/<component>:<tag>

OPA bundles

endpoint: /opa/<project>/<repo>/
publish:  PUT /api/bundles  (raw .tar.gz of .rego + data.json + .manifest)
pull:     GET /<name>/<version>/bundle.tar.gz  (revision served as ETag)

Bazel

endpoint: /bazel/<project>/<repo>/  (BCR-shaped module registry)
cache:    /bazel/<project>/<repo>/{ac,cas}/<sha256>  (HTTP remote-cache protocol)

The same repo also speaks Bazel's HTTP remote-cache protocol — point a build at it with your orbr_… token as HTTP Basic credentials (via ~/.netrc or --remote_header):

build --remote_cache=https://registry.example.com/bazel/acme/buildcache
build --remote_upload_local_results=true

CAS uploads are hash-verified server-side (a body that doesn't match its claimed SHA-256 is rejected with 400). Cache entries are ordinary artifacts, so eviction is a retention policy: attach a max_age_days rule to the cache repo and the hourly sweeper hard-deletes entries not re-written since the cutoff — a hot key that gets re-uploaded after a miss resets its clock. Per-entry size limits: 8 MiB for action-cache records, 4 GiB per CAS blob. Security blocks and the pull-gate apply to cache downloads like any other artifact; cache blobs are not routed through the vulnerability-scan queue.

Conan

endpoint: /conan/<project>/<repo>/
config:   conan remote add orbital ...

Cocoapods

endpoint: /cocoapods/<project>/<repo>/
config:   Podfile → source 'https://registry.example.com/cocoapods/<project>/<repo>/'

The adapter serves the trunk-CDN layout pod install reads natively (CocoaPods ≥ 1.8): CocoaPods-version.yml advertises prefix_lengths: [1, 1, 1], so clients compute the same three-character MD5 shards the registry stores (all_pods_versions_<a>_<b>_<c>.txt + Specs/<a>/<b>/<c>/<name>/<version>/<name>.podspec.json). all_pods.txt (full pod list) and deprecated_podspecs.txt (specs published with deprecated: true or deprecated_in_favor_of) complete the CDN file set. All plain-text index files carry strong ETags and answer If-None-Match with 304, so repeated pod repo update runs are cheap. Publish via POST /api/v1/pods (JSON podspec, pod trunk push-shaped); optional source tarball hosting under /pods/<name>-<version>.tar.gz.

Puppet Forge

endpoint: /puppet/<project>/<repo>/
status:   Experimental — some Forge metadata fields are stubbed.

Chef Supermarket

endpoint: /supermarket/<project>/<repo>/

The adapter speaks the Supermarket API surface Berkshelf, Policyfile, and knife supermarket all resolve against. Point a Berksfile at the repository root and the cookbook graph resolves from /universe:

ruby
source "https://registry.example.com/supermarket/acme/cookbooks"

cookbook "myapp"

/universe lists every cookbook version with its dependency constraints (location_type: supermarket), is generated from the stored cookbook metadata on read, and carries a strong ETag so the fetch Berkshelf and Policyfile do at the start of every run revalidates with 304 when nothing changed. Version routes accept both the dotted and the underscore-escaped form Berkshelf requests (/api/v1/cookbooks/<name>/versions/1_2_3), and "latest" is ordered numerically per version segment, not lexicographically. Upload with knife supermarket share (multipart) or plain curl (raw .tar.gz body); dependencies are read from the archive's metadata.json, falling back to scraping metadata.rb.

Bower

endpoint: /bower/<project>/<repo>/
status:   Deprecated (2026-08-15) — Bower itself has been unmaintained
          upstream since 2017. Register/lookup/search keep working for
          existing repos (not hard-blocked); every `POST /packages`
          response carries an RFC 7234 warn-code 299 notice. Don't start
          a new repo on this format — see the migration note below.

Bower has no artifact storage of its own — the registry is a bare {name → git URL} lookup table (GET /packages/{name}, POST /packages to register, GET /packages/search/{query}). bower install resolves a name against this table, then clones the git URL directly; orbitalreg never sees the package content itself. That protocol is complete and has been since the adapter shipped — the problem is Bower the client/ecosystem, not this registry.

Migrating to npm. Bower was deprecated by its own maintainers in 2017 in favor of npm/Yarn; there is no newer "Bower 2". The npm adapter (/npm/<project>/<repo>/) is the direct replacement:

  1. Run bower-away (or equivalent) in the consuming project to generate a package.json from bower.json — it maps dependencies/devDependencies and drops Bower-only fields (main as an array, ignore, moduleType).
  2. Point npm install / .npmrc at this registry's npm endpoint instead of bower install at the Bower one.
  3. Existing bower.json-based consumers keep working against this registry in the meantime — deprecation does not mean removal.

JetBrains Marketplace

endpoint: /jetbrains/<project>/<repo>/
index:    updatePlugins.xml   (generated per read, strong ETag,
                               optional ?build=IU-241.14494 filter)
publish:  POST api/plugins    (multipart file= or raw body)

Point the IDE at the repository under Settings → Plugins → Manage Plugin Repositories using the updatePlugins.xml URL. The index is composed from the descriptor (META-INF/plugin.xml) each upload is cracked open for, and carries the full custom-plugin-repository element set the IDE parses: <name>, <description>, <change-notes>, <vendor email= url=>, non-optional <depends> entries, and <idea-version since-build= until-build=> compatibility ranges. Compatibility filtering works on both ends: pass ?build=<PRODUCT>-<build> (e.g. IU-241.14494) and the server keeps only the newest version of each plugin whose since/until range matches that build — an older-but-compatible version is served when the newest one requires a newer IDE; without the parameter every plugin's newest version is listed and the emitted <idea-version> bounds let the IDE filter client-side. until-build="241.*" branch wildcards and product-prefixed build numbers are handled. The index responds 304 to If-None-Match, so the refetch the IDE does on every plugin check revalidates instead of re-downloading. Upload with Gradle's publishPlugin task or curl -F file=@plugin.zip; both the outer-ZIP (lib/*.jar) and plain-JAR plugin shapes are parsed.

VSIX (VS Code)

endpoint: /vsix/<project>/<repo>/
status:   Experimental — namespace/publisher creation is a stub; upload and
          download work.

P2 (Eclipse)

endpoint:  /p2/<project>/<repo>/
indexes:   p2.index, content.xml|.jar, artifacts.xml|.jar (generated per read)
composite: compositeContent.xml|.jar, compositeArtifacts.xml|.jar
           (kind=virtual repos only)
mirrors:   PUT/GET/DELETE /mirrors.xml  ->  p2.mirrorsURL in artifacts.xml

Composite repositories. A kind: virtual p2 repo is served as a native p2 composite: its p2.index steers Eclipse at compositeContent.xml / compositeArtifacts.xml, whose <children> list is derived from the virtual's member list (in priority order, as relative ../<member> child locations). Point Eclipse's Install New Software… dialog at the virtual repo's URL and the client resolves and installs across all members — nothing is aggregated server-side, and membership changes are live on the next client refresh. p2.atomic.composite.loading=true is advertised, so a failing child surfaces as an error instead of a silent partial view. On kind: local/remote repos the composite endpoints answer 404, which Eclipse's factory probing treats as "not a composite".

Mirrors. Publishing a standard Eclipse mirrors document (<mirrors><mirror url="…" label="…"/></mirrors>, absolute http(s) URLs) to PUT /mirrors.xml makes artifacts.xml advertise it via the p2.mirrorsURL property; Eclipse's mirror selector then picks download mirrors per artifact. DELETE /mirrors.xml reverts to direct downloads.

NVIDIA NIM / Skills

endpoint: /nim/<project>/<repo>/   (NGC-catalog-compatible model/skill registry)
config:   ngc config set -> api_url = https://...example.com/nim/acme/all
note:     NIM container images flow through the Docker endpoint at /v2/...

AI / agent ecosystems

Agent/Skill Packages

endpoint: /agentskill/<project>/<repo>/
publish:  POST /<project>/<repo>/  (raw .tgz; manifest.json or agent.yaml at
          the archive root carries name/version/kind + tools/permissions/
          egress/dependencies; optional root SKILL.md becomes the description)
list:     GET /<project>/<repo>/<name>            (versions + latest resolution)
get:      GET /<project>/<repo>/<name>/<version>   ("latest" alias supported)
pull:     GET /<project>/<repo>/<name>/<version>/download
status:   Experimental — new in this release (roadmap item 149). Publish/
          pull/promote work and Pull-Gate/security-blocks/signature
          verification apply with no format-specific special-casing. A
          Skill-Browser UI (repo detail page -> Packages tab) renders the
          manifest, tools/permissions/egress, and SKILL.md per package
          (Phase B); there is no MCP server yet (Phase C), and packages
          are gzip-tar (.tgz) only (no tar.zst).

VCS proxies

Git LFS

endpoint: /lfs/<project>/<repo>/
config:   .lfsconfig -> [lfs] url = "..."

Generic VCS (archive mirror + go-module proxy)

endpoint: /vcs/<project>/<repo>/
publish:  PUT  /<vcs>/<org>/<name>/archive/<ref>.tar.gz   (mirror mode; also .tgz/.zip)
pull:     GET  /<vcs>/<org>/<name>/archive/<ref>.tar.gz
refs:     GET  /<vcs>/<org>/<name>/refs   |   /info
go get:   GET  /<vcs>/<org>/<name>[/subpkg]?go-get=1      (go-import meta, `mod` type)
goproxy:  GET  /<vcs>/<org>/<name>/goproxy/<module>/@v/…  (list, @latest, .info, .mod, .zip)
purpose:  front source-archive fetches (Bazel, CMake FetchContent, Meson wrap,
          `go get`) with the artifact cache instead of every CI worker
          hitting the git host directly.
status:   Supported. `go get <host>/vcs/<project>/<repo>/git/<org>/<name>`
          resolves via the built-in GOPROXY subset: semver-tagged stored
          archives appear in @v/list, and `go get module@<commit-sha>`
          pins a commit (answered as a v0.0.0 pseudo-version). On a
          local miss, repositories of kind `remote` cache-through-fetch
          the archive from the upstream host (GitHub/Gitea and GitLab
          URL layouts auto-detected; upstream credentials from the
          remote-repo config cover private repos). Full commit-SHA refs
          are pinned as immutable artifacts; branch/tag refs ride the
          revalidating upstream cache. Set GOPRIVATE for these module
          paths — sum.golang.org cannot know a private registry's
          versions. Not proxied: live git clone/fetch protocol,
          @v/list enumeration of upstream tags, and branch-name
          version queries (a tarball fetch cannot prove which commit a
          branch resolved to — pin the SHA instead).

Generic blob

endpoint: /api/v1/artifacts/<project>/<repo>/...
purpose:  content-addressable raw artifact store (SHA-256), for anything
          without a native package format

Detection coverage

Most formats above run through Trivy / Grype / OSV.dev / Syft. The few that don't (Vagrant, Bazel, Cocoapods, Bower, JetBrains, VSIX, P2, NVIDIA NIM, Ollama, Argo Workflows, Kustomize, OPA, Deno, Homebrew, Nix, Puppet, Supermarket) lack reliable upstream advisory data; they still get SBOM generation via Syft when format-conventionally a package contains a manifest.

Adding a new format

The format-adapter contract is a Go interface in api/internal/formats/<format>/. Each adapter implements:

  • Upload(ctx, *http.Request) (*Artifact, error)
  • Download(ctx, path, version) (io.Reader, error)
  • Index(ctx, repo) (Indexable, error)
  • Metadata(ctx, artifact) (Metadata, error)

The integration-test harness in tests/integration/ boots the API against a real Postgres + MinIO and exercises every adapter against its native client tool (where one exists). Adding a format means landing the adapter, the integration fixture, and a row in PACKAGE-FORMATS-ROADMAP.md.

Released under the Apache-2.0 License.