# Packr Registry — Full Reference > The Verdaccio that actually works in CI. A CI-first private npm registry written in Go, with OAuth device flow, fine-grained scoped tokens, AI agent endpoints, and a Next.js dashboard. ## What it is Packr is a private npm registry designed for modern CI/CD workflows. It exists because existing registries (Verdaccio, GitHub Packages, JFrog) were built for interactive human workflows and make CI painful: session cookies, captchas, device codes that require a TTY, permission models designed for organizations rather than jobs. Packr solves this with: - **Stateless JWT tokens** from a single POST to /-/v1/login (or device flow for interactive CLI login) - **Scoped permissions** — each token has a role (ci-readonly, ci-publish, maintainer, admin) and optional scope restriction - **Structured error responses** — JSON with error code, message, and status - **Zero configuration for the npm client** — standard .npmrc with an env var placeholder works out of the box ## Architecture - **Registry server** — Go, stateless, runs on Fly.io or any container host - **Dashboard** — Next.js 16 App Router, deployed to Vercel - **CLI** — Go binary (`packr-cli`) for login, token management, publishing, agent queries - **SDK** — `@packr/agent` TypeScript SDK for AI agent integration - **MCP server** — `@packr/mcp-server` for Claude and Cursor integration ### Storage Two-tier: - **Metadata DB** — SQLite (default, single-instance) or PostgreSQL (multi-instance, Supabase or Neon). Schema: packages, versions, users, tokens, sessions, webhooks, audit_events, subscriptions, device_codes, oauth_accounts, org_members, usage_metrics, package_docs. - **Blob store** — local filesystem (dev), S3, Supabase Storage, or Backblaze B2 (all S3-compatible). Tarballs are served via presigned URL redirects with immutable cache headers. ## Authentication ### JWT tokens (CI) `POST /-/v1/login` with { name, password } returns a 30-day JWT. The token claims: ```json { "org_id": "default", "user_id": "kristianmandrup", "permissions": ["read", "publish", "unpublish"], "scopes": ["@blueforge-studio"], "exp": 1778461024, "iat": 1775869024 } ``` Permissions are enforced by `RequirePermission` middleware. The scope restriction is checked against the package being published. Empty scopes means all scopes (backward-compatible with legacy tokens). ### OAuth device flow (RFC 8628) For CLI login without embedding a browser: 1. CLI calls `POST /-/v1/device/authorize` with a provider name, gets `device_code` + `user_code` 2. User opens `/cli/auth` in a browser, enters the code 3. Dashboard redirects to the OAuth provider (GitHub, Google, GitLab, OIDC, or forge-auth) 4. Provider callback hits `/api/auth/callback/:provider` with an authorization code 5. Dashboard calls registry's `POST /api/v1/admin/device/callback` with the code 6. CLI polls `POST /-/v1/device/token` and gets the JWT Supported providers: github, google, gitlab, oidc (generic Keycloak/Okta/Azure AD), forge-auth (BlueForge SSO proxy). ### Session tokens (dashboard) The Next.js dashboard uses HMAC-signed session cookies (packr_session) created via `POST /api/v1/admin/sessions`. The `AdminAuthMiddleware` accepts both session tokens and JWTs — falling back to JWT validation when session validation fails. This lets the CLI call admin API endpoints using its device-flow JWT (e.g. `packr-cli token create --registry ...`). ## Token permissions ### Permissions - `read` — download packages, view metadata, search - `publish` — publish new versions, update dist-tags, deprecate - `unpublish` — remove versions (within 72h window) - `admin` — manage tokens, webhooks, ownership, profiles ### Preset roles | Role | Permissions | Typical use | |------|-------------|-------------| | ci-readonly | read | CI install jobs | | ci-publish | read, publish | CI release workflows (scoped) | | maintainer | read, publish, unpublish | Human maintainers | | admin | all | Platform admins | ### Scope restriction Tokens can be restricted to specific npm scopes. A token with `scopes: ["@blueforge-studio"]` can only publish to that scope — attempts to publish to `@other-org/*` return 403. Empty scopes = all scopes allowed. ## Agent API The agent endpoints are designed for AI agents and codegen tools to query the registry efficiently: - `GET /agent/:pkg` — package info with quality score (0-100), download count, extracted capabilities, dependencies, trust signals (hasCI, hasTests, hasTypes, packageSize) - `GET /agent/search?q=:capability` — capability-based search (e.g. "authentication", "HTTP server") - `GET /agent/compare?packages=a,b,c` — side-by-side comparison with a recommended pick - `GET /agent/:pkg/deps` — direct dependency graph - `GET /agent/:pkg/recommendations` — packages commonly used alongside this one (co-occurrence analysis) - `GET /agent/:pkg/docs` — AI-generated API documentation (requires `ANTHROPIC_API_KEY` + `DOC_GEN_ENABLED=true` on the registry) Quality scoring factors: TypeScript types (+20), tests (+15), CI config (+10), README length (+10), version maturity (+10/5), freshness (+15), base (+15). ## Publishing `PUT /@scope/pkg` with npm CLI format (metadata JSON + gzipped tarball separated by \n\n) or multipart/form-data. The handler: 1. Validates the package name and version 2. Checks the token has `publish` permission 3. Checks the token scope restriction matches the package scope 4. Looks up the org (if the scope matches an org slug) and verifies membership — skipped for CI tokens (user_id starts with "web:") 5. Enforces plan limits — skipped for CI tokens 6. Upserts the package and version in the DB 7. Saves the tarball to blob storage with immutable cache headers 8. Logs a `package.published` audit event 9. Triggers async AI doc generation (if enabled) 10. Fires webhook deliveries (if any are configured for the event) ## Webhooks Configured per-user via the admin API. Events: `package.published`, `package.unpublished`, `package.deprecated`, `token.created`, `token.revoked`. Deliveries are logged in the `webhook_deliveries` table. Failed deliveries (status ≥ 400 or connection error) are retried with exponential backoff: 1 minute, 5 minutes, 30 minutes, 2 hours, 24 hours. After 5 attempts they're marked permanently failed. A background worker polls the retry queue every 30 seconds. Payloads are HMAC-SHA256 signed in the `X-Packr-Signature` header using the webhook's secret. ## Organizations and teams - Organizations have a slug, display name, and members - Each member has a role: admin, maintainer, or reader - When publishing to a scope that matches an org slug, the publisher must be a member with maintainer or admin role (unless they're using a CI token) - API: `GET/POST /api/v1/admin/orgs`, `GET/POST/DELETE /api/v1/admin/orgs/:slug/members`, `GET /api/v1/admin/users/me/orgs`, public `GET /api/v1/orgs/:slug/packages` ## Subdomain routing The Next.js proxy (`packages/site/proxy.ts`) detects org subdomains like `myorg.packr.dev` and rewrites the path to `/org/myorg`. This gives each org a vanity URL without additional routing logic. ## Geographic replication Multi-region Fly.io deployment with PostgreSQL read replicas. Set `PRIMARY_REGION` in fly.toml and `REPLICA_DATABASE_URL` in secrets. Reads serve from the nearest replica, writes forward to primary. Health endpoint reports the serving region. ## Billing Stripe integration. Webhook handler at `/api/billing/webhook` processes: - `checkout.session.completed` — creates subscription record - `customer.subscription.updated` — updates plan/limits - `customer.subscription.deleted` — downgrades to free - `invoice.payment_failed` — sets past_due Plans: Free (3 packages, 1 scope), Solo ($10/year, 20 packages, 5 scopes), Team ($50/year, 100 packages, unlimited scopes), Enterprise (custom). ## Installation ### Self-hosted (Docker) ```bash docker run -p 4873:4873 \ -e JWT_SECRET=$(openssl rand -hex 32) \ -e SESSION_SECRET=$(openssl rand -hex 32) \ -e INTERNAL_API_SECRET=$(openssl rand -hex 32) \ -v packr-data:/data \ ghcr.io/kristianmandrup/packr-registry ``` ### Kubernetes (Helm) ```bash helm install packr ./helm/packr \ --set secrets.JWT_SECRET=$(openssl rand -hex 32) \ --set secrets.SESSION_SECRET=$(openssl rand -hex 32) \ --set ingress.enabled=true \ --set ingress.hosts[0].host=packr.example.com ``` ### Fly.io See `fly.toml` — supports multi-region scaling with `fly scale count 1 --region iad`. ## CLI usage ```bash # Login via OAuth device flow (opens browser) packr-cli login --provider github --registry https://packr.example.com # Create scoped tokens packr-cli token create ci-read --role ci-readonly packr-cli token create ci-publish --role ci-publish --scope @myorg # Initialize a project packr-cli init --scope @myorg # Agent queries packr-cli agent info @myorg/pkg packr-cli agent search "authentication" packr-cli agent deps @myorg/pkg ``` ## Links - **Live site**: https://packr.blueforge.studio - **Live API**: https://api.packr.blueforge.studio - **Source**: https://github.com/kristianmandrup/packr-registry - **OpenAPI spec**: https://api.packr.blueforge.studio/openapi.json - **Health check**: https://api.packr.blueforge.studio/health