Skip to content

Architecture ​

OpenArtifact is one Go binary (cmd/server) that serves the REST API, the native package-manager protocols and an embedded web UI, backed by PostgreSQL and a pluggable blob store.

Layout ​

PathContents
cmd/serverHTTP server entrypoint: route wiring, startup/shutdown, bootstrap admin
cmd/oaCLI client
cmd/resetpwAdmin password reset tool
internal/apiREST handlers (/api/v1/*), upload handlers (upload_{deb,rpm,npm,maven}.go), openapi.json
internal/format/<fmt>Native protocol handlers per package format (/repos/{name}/..., /v2/...); base holds the shared handler plumbing, filerepo the one handler behind ivy and sbt, formattest test helpers
internal/middlewareSecurity headers, recovery, request ID, tracing, metrics, logging, CORS, IP allowlist, rate limit, RBAC, CSRF, audit
internal/authSessions, tokens, permission evaluation, OIDC/SAML, SSO provisioning, bootstrap
internal/databaseDatabase interface (interfaces.go), Postgres implementation (postgres*.go), sqlc code in dbsqlc/, queries in queries/*.sql
internal/storageBlob stores: filesystem, S3, GCS, Azure, OCI; SafeDeleteBlob refcounting
internal/workerBackground workers: per-format metadata regeneration, cleanup, Docker registry GC, proxy cache, auth purge, AI analysis, blob deletion sweeper (deferred byte removal, see storage)
internal/repodispatchChi middleware that serves proxy and group repositories in front of the per-format routes
internal/proxy, internal/groupProxy-repository fetch/cache service; group (ordered, read-only merge of members) service
internal/pypisimplePyPI simple API (PEP 503/691) parsing, rendering, content negotiation and path/name rules shared by the pypi proxy and group
internal/repotypeWhich formats may be proxy or group repositories (shared by repo creation and dispatch)
internal/ocipathParses /v2/<name>/... paths; shared by RBAC and the docker handler so both agree on the repository
internal/importmetaAdmin-only X-OA-Import-Created-At header that preserves origin timestamps during migrations
internal/clientsetupNative client URLs and configure/publish snippets per format (Go port of web/src/lib/installCommands.ts; keep both in sync; a shared golden file in testdata/ makes either side's tests fail on drift)
internal/quarantineBlocks downloads of artifacts flagged by vulnerability/secret scans
internal/quotaReserves/releases repository storage quota on upload, delete and cleanup-policy deletes (the database's ReconcileStorageUsage recounts the counters from the package rows)
internal/netutilSSRF-safe outbound HTTP helpers (webhooks, proxy upstreams)
internal/cron5-field cron expression parser (UTC fire times)
internal/urlparamsVars(r): chi URL parameters as a map
internal/{backup,webhook,search,vuln,sbom,license,secrets,ai,mcp,audit,metrics,config}Feature subsystems
internal/cryptoAES-256-GCM encryption of stored secrets (OPENARTIFACT_SECRET_KEY)
pkg/*Format parsers and helpers: deb, rpm, npm, nuget, conan, apk, maven, helm, rubymarshal, compactindex, gpg, delta, errors, logger
migrations/Embedded SQL migrations, applied at startup
web/Vue 3 + TypeScript + Tailwind SPA (bun), embedded via web/embed.go
tests/Go handler tests (many need -tags=integration); tests/integration/ uses testcontainers
bench/oabench load generator and run.sh driver; see performance
tools/Separate Go module pinning sqlc, govulncheck, actionlint
docker/Dockerfile and compose files

Request flow ​

text
client
  │
  ▼  global chain, in this order (cmd/server/main.go)
  SecurityHeaders → Recovery → RequestID → Tracing → Metrics → Logging → CORS
    → [IPAllowlist] → [RateLimit] → AuthAudit
  │
  ├─ bare router (no auth): /health /ready /metrics*, login/logout, OIDC/SAML
  │    callbacks, public SSO list, system info, OpenAPI
  │
  └─ authedAPI, mounted at "/":  RBAC → importmeta → CSRF → Audit
       ├─ /api/v1/*             → internal/api handlers, search, MCP mount
       ├─ /v2/*                 → docker handler (path parsed by ocipath)
       ├─ /repos/{name}/*       → repodispatch ─┬─ hosted: format handler
       │                                        └─ proxy/group: internal/proxy, internal/group
       └─ anything else         → embedded SPA (NotFound handler)
                                       │
            format handler / API ──────┴──► quota, quarantine → storage (BlobStore) + database (Database)

[...] middlewares are only installed when configured. /metrics answers 401 unless METRICS_AUTH_TOKEN is set or METRICS_ALLOW_ANONYMOUS=true.

RBAC is the authorization gate: /api/v1/* maps to the manage action, /repos/{name}/... to read/write by HTTP method, /v2/... via ocipath. Page loads of the SPA bypass RBAC (GET/HEAD only). Details: ../security/rbac.md.

Routing invariant ​

Every repo-scoped format handler registers relative routes on the single /repos/{name} subrouter in cmd/server/main.go (RegisterRepoRoutes(r)). Never register an absolute /repos/{name}/... route on authedAPI: the subrouter mount shadows it (404) or the route hijacks the subrouter.

  • Routes that need the same pattern are dispatched explicitly. PUT /* is shared by npm publish and GOPROXY upload: the catch-all calls gomodHTTP.HandlePut when formatgomod.IsUploadPath matches the tail, npmUpload.HandlePublish otherwise.
  • Formats whose wildcard paths would shadow others are mounted on isolated prefixes: /repos/raw/{name}, /repos/maven/{name}, /repos/sbt/{name}, /repos/ivy/{name}. All /repos/... mounts sit behind repodispatch. Proxy repositories are supported for maven, sbt, ivy, deb, rpm, pacman, docker, npm, pypi, cargo, go, helm, nuget, conan, rubygems, conda, terraform and raw; groups for maven, sbt, ivy, deb, rpm, pacman, docker, npm, pypi, cargo, go, helm, nuget, conan, rubygems, conda, terraform and raw (internal/repotype). Docker proxies and groups are not served under /repos/{name}: repodispatch.OCIMiddleware sits in front of the /v2 routes and hands them to proxy.ServeOCI / group.ServeOCI (bearer-token handling in internal/proxy/oci_auth.go).
  • The Terraform/OpenTofu registry needs two routes outside RBAC on the bare router: the service discovery document (/.well-known/terraform.json) and the ticket download route, because the clients send no credentials to download URLs (terraform). Everything else, including the publish API (/_api/...), is on the /repos/{name} subrouter. The ticket route stays in the hosted handler for proxy repositories too: a ticket whose repository is a proxy is handed to proxy.Service.ServeTerraformDownload (terraform.ProxyServer), which fetches the file from its upstream host on first use; the tickets are minted by terraform.Minter with the same HMAC key.
  • Group repositories read hosted members through in-memory routers of the same Maven, npm, PyPI, cargo, Go, Helm, NuGet, Conan, RubyGems, conda, Terraform and raw handlers.
  • cmd/server/routes_test.go guards the registration order against chi Mount() panics.

Storage and database ​

  • Blob store (storage.BlobStore): Put/Get/Delete/Exists/GetMetadata/List/GetSize/ExistsBySHA256/PresignURL. Blobs are content-addressed by sha256 and deduplicated across repositories, so deletes go through storage.SafeDeleteBlob (refcounting). Backends are chosen by storage.type; see ../operations/storage.md. The server wraps the store in NewMetricsBlobStore for Prometheus metrics.
  • Presigned URLs (../operations/storage.md#presigned-urls): the S3 and Azure stores implement the optional storage.Presigner interface, and every store implements storage.StagingStore and storage.MultipartStagingStore (S3, Azure natively; filesystem and OCI through the HMAC relay in internal/storage/relay.go) (MetricsBlobStore forwards them). The presign.Store decorator in internal/presign wraps the blob store and decides, per request, whether to answer a blob download with a 307 to a presigned GET URL (global switch, per-repository presign_downloads, presignMinSize, backend support). Handlers built on base.Handler call the TryPresign hook after the authorisation check; handlers without it (docker, deb, rpm) call presign.Try directly. Metadata responses never use it. Presigned raw uploads live in internal/api/presigned_uploads.go (create and complete), share the write path with the raw PUT through raw.Commit, and stale staging objects are removed by worker.PresignedUploadWorker.
  • Database (database.Database): the interface in internal/database/interfaces.go is what handlers and workers depend on; postgres*.go implements it, hand-written SQL goes in queries/*.sql and sqlc generates dbsqlc/ (never edited by hand). ListRepoComponents unions every per-format package table, so a new format needs a branch there.
  • Migrations are migrations/*.sql, embedded (migrations/embed.go) and applied at startup unless MIGRATE_ON_START=false; openartifact -migrate applies them and exits. They are forward-only: the migrator aborts on checksum drift of an applied file.

Workers ​

Started by the app in cmd/server (app.go is the startup/shutdown sequence, the app_*.go files hold its steps) and stopped in order on shutdown: HTTP first, then the fire-and-forget background tasks (async backups, restores and scans, startup metadata regeneration; they run on internal/bgtask, get a context cancelled at shutdown and are waited for at most 15 s), then the workers, then the audit flush, DB last:

WorkerRole
RPM / Deb / Maven / npm metadataRegenerate per-format repository metadata after uploads and deletes
Proxy cacheEvicts expired proxy cache entries (and their blobs once unreferenced), honoring each repository's max-stale window
CleanupApplies cleanup policies and reclaims blobs no package row references any more
Docker registry GCDeletes untagged Docker manifests past a grace period and the blobs only they referenced (details)
Auth purgeDeletes expired sessions and long-expired or revoked API tokens
AI analysisOptional; runs when AI is enabled
Webhook dispatcherDelivers events (artifact.uploaded, artifact.deleted, ...)

Workers that take Postgres advisory locks hold them on a pinned connection (internal/worker/lock.go), so they are safe with multiple server instances.

Web UI ​

web/ is built with bun/Vite into web/dist, embedded with go:embed in web/embed.go, and served by the authedAPI NotFound handler (client-side routing). Unmatched /api/ paths return a JSON 404, non-GET/HEAD requests return 405. In binaries built with -tags dev, setting OPENARTIFACT_DEV makes the server proxy the UI to the Vite dev server (http://localhost:5173); see README.

Free software under the AGPL-3.0-or-later.