Skip to main content
Passport ships as one container image. The same image runs locally and in production; everything is configured from environment variables, so there is nothing secret baked into the image. In self-hosted mode the hosted service never touches your data.

Build and run

There is no public pull-by-tag registry image today. Build from your Passport source checkout. Supply PASSPORT_ENCRYPTION_KEY through your secret manager or local environment before running the container; generate it once and retain it across restarts and upgrades.
Terminate HTTPS at the configured PUBLIC_URL. This initial example disables new workspace signup. Before inviting new users to create a workspace, configure verified email or Google/GitHub login, then explicitly choose PASSPORT_SIGNUP_MODE=open or domain. For a new Postgres database, supply DATABASE_URL through your environment and run the compiled migrator from the same image before starting the application:
Pass the same database and any required TLS settings to the migrator and application; add -e DATABASE_URL to the application command. Startup checks schema compatibility and never runs migrations itself. Existing deployments must follow the supported upgrade sequence in docs/DEPLOY.md from the source repository, including any required intermediate releases. Without Postgres, state lives under /data and must be on a mounted volume; use a single application instance. For production bring-up checklists, see docs/DEPLOY.md and docs/OPERATIONS.md in the source repository.

What you need

The container

One Passport container, listening on PORT (default 8790).

Postgres

A Postgres database for durable, backed-up state that survives redeploys. Without it, state lives in a JSON file you must persist on a mounted volume.

Production posture the server enforces at boot

When NODE_ENV=production, Passport fails closed: a misconfigured deployment refuses to boot rather than run with plaintext secrets, open unverified signup, or mock data on a real domain. Startup exits with a named error unless all of the following hold:
  • PASSPORT_ENCRYPTION_KEY contains at least 32 bytes of random key material. Without it, upstream OAuth tokens and company keys would be stored in plaintext.
  • PUBLIC_URL is set and starts with https://. Otherwise OAuth callbacks and CORS would trust the Host header.
  • PASSPORT_DEMO is explicit, either off (recommended) or on.
  • PASSPORT_BILLING_MODE is explicit. Use self-hosted to intentionally unlock Enterprise without Stripe, or hosted with a complete Stripe configuration.
  • PASSPORT_SIGNUP_MODE is explicit: off, open, or domain. If signup is enabled, configure either verified email or Google/GitHub identity login. With PASSPORT_EMAIL_VERIFY=off, production becomes provider-only: typed email addresses cannot create sessions or workspaces.
A single escape hatch, PASSPORT_UNSAFE_OVERRIDE=i-accept-the-risk, bypasses these checks for a throwaway staging bring-up only when RAILWAY_ENVIRONMENT_NAME or SENTRY_ENVIRONMENT explicitly identifies staging. It logs every time it is used and is rejected for production or an unspecified environment.

Core

Persistence

Security and encryption

Key rotation. Set PASSPORT_ENCRYPTION_KEY to the new key, move the old key into PASSPORT_ENCRYPTION_KEYS, deploy, then from the repository run cd server && npm run rekey to re-encrypt workspace documents, live resumable operation checkpoints, queued delivery payloads, and global leads under the new primary. Then drop the old key and run rekey once more to remove retired early-access blind-index aliases. A value whose key is absent is left untouched, never overwritten, so nothing is lost mid-rotation.

Signup and demo

Email

Email is provider-agnostic. Setting a transport enables features that require email (verified sign-in, invites, request notifications).

Single sign-on (OIDC) fallback

Per-workspace SSO is configured in Settings (see SSO). These environment variables provide a global fallback for the single-tenant or appliance case.

Connecting upstream accounts

You can also let a person sign into Passport itself with Google or GitHub by setting the PASSPORT_LOGIN_GOOGLE_* and PASSPORT_LOGIN_GITHUB_* pairs. A provider is available only when both its client ID and secret are set.

Billing (Stripe)

Set PASSPORT_BILLING_MODE=self-hosted to intentionally unlock Enterprise for every workspace without Stripe. Production refuses an omitted mode, and self-hosted mode rejects dormant Stripe settings so a deployment cannot silently mix the appliance and hosted billing contracts. For a hosted service, set PASSPORT_BILLING_MODE=hosted and configure the key, webhook secret, and both distinct Stripe price IDs. Production refuses to start when any part is missing or inconsistent. It also forbids redirecting STRIPE_API_BASE away from Stripe; that override is only for local tests.

Gateway rate limits

Per-workspace and per-token caps on the gateway, plus a per-replica concurrency guard. A shared rate-counter failure does not block the gateway; the local concurrency guard remains a finite hard bound and returns retryable backpressure at capacity. These request counts are upper bounds. A separate memory guard can return 503 with Retry-After: 1 sooner when workspace snapshots and provider payloads would exceed the replica’s available memory budget. It uses the lower of the V8 heap ceiling and the OS process constraint, with a smaller reservation for sign-in status and approval review/decisions so waiting calls do not consume their workspace’s entire approval capacity. The passport_request_memory_admission_rejected_total counter records refusals by admission phase without member or workspace labels. JSON, form, and Stripe webhook bodies enter the same process memory budget before parsing. Compressed and chunked uploads reserve against the decoded body limit; parsed values remain reserved while their request or provider work retains them. Excess uploads receive 503 with Retry-After: 1. These memory checks can refuse a body below its individual size limit when the replica has insufficient capacity. The HTTP server uses a 60-second request-upload timeout and a 15-second header timeout. These upload limits do not shorten the configured approval or provider response deadlines.

Fail-closed operational bounds

The following operator-controlled limits accept only base-10 integers in their inclusive supported range, except the logical database budget thresholds, which accept finite base-10 decimals from 0–1. Passport refuses to start when a value is blank, fractional where an integer is required, non-finite, zero where disallowed, out of range, or inconsistent with a related bound; it never silently falls back from a malformed production setting. PASSPORT_APPROVAL_ELICITATION is a separate on/off switch (default on). It controls whether Passport asks for an action approval inside the calling agent over MCP elicitation. With off, approvals are answered only in the Passport app; the hold, the pending result, and the CLI retry loop are unchanged either way.

Metrics, monitoring, and paging

pg_database_size() is a logical PostgreSQL footprint, not physical host or provider-volume usage. Passport therefore exposes passport_database_logical_size_bytes, passport_database_logical_budget_bytes, and passport_database_logical_budget_utilization_ratio; the configured budget is for growth planning only. A self-hosted operator must monitor filesystem-volume capacity independently at the host/provider layer. The deprecated passport_database_storage_* time series preserve dashboard compatibility but carry the same logical values and explicitly do not represent volume capacity.

Workspace mutable-state ceilings

Each workspace’s mutable control-plane document has a fixed 8 MiB serialized ceiling. Live state is capped at 10,000 members, 5,000 apps, 1,000 bundles, 2,000 app requests, 10,000 invites, 10,000 join requests, and 5,000 agent keys. Terminal requests, invites, join requests, and agent keys are retained for at most 90 days and 500 rows per kind; a terminal request retains at most 100 requesters. Passport compacts terminal history before every file or PostgreSQL write. The append-only activity/audit log is separate and remains authoritative. At 80% of any hard ceiling, Passport increments passport_workspace_document_pressure_observations_total{reason}. Compaction events and removed bytes are exposed through passport_workspace_document_compactions_total{reason} and passport_workspace_document_compacted_bytes_total{reason}. If live state still exceeds a ceiling after compaction, the write fails closed with retryable 503 backpressure and increments passport_workspace_document_capacity_rejected_total{reason}. Alert on any pressure increase before raising a limit; see the repository runbook for the response procedure. Sentry is entirely optional for a self-hosted deployment. With both DSNs unset, no Sentry telemetry leaves the instance. Browser DSN, environment, sampling, and verification settings are read from the running service through /api/browser-runtime-config.js. Changing those settings requires updating the service environment, not rebuilding the browser assets. Release identity remains an image-build input: build with the exact commit as SENTRY_RELEASE and keep the same value at runtime. A mismatched runtime release is rejected. For source maps, first run the web build locally with SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT, SENTRY_REPOSITORY, and that same release, then build the image without the token. Omitting that local upload means browser frames have no source maps. The entrypoint removes any platform-injected upload token before Node starts. When enabled, Passport sends normalized route/status/latency data, stack traces, operational spans/logs/metrics, app/tool/agent identifiers and outcomes, and opaque member/workspace IDs. Collection of query strings, cookies, authorization headers, HTTP bodies, database statements, tool inputs/outputs, and AI content is disabled. Passport does not deliberately attach names or emails, and its scrubbers redact email-like values and credential patterns found in diagnostic text. Browser Replay is intentionally disabled because its rrweb metadata can serialize hash/query URLs before event hooks can scrub them. Do not enable it until URL metadata can be guaranteed safe before serialization. Sentry retention is controlled by your Sentry plan and organization settings, independently of Passport’s event-log retention. For staging verification of Sentry releases and source maps, see docs/DEPLOY.md and docs/OPERATIONS.md in the repository. The bounded defaults avoid exhausting a shared span allowance; Express middleware/router bookkeeping spans are omitted while request roots, database, MCP, and outbound HTTP spans remain. If logs and metrics continue but spans stop, check the Sentry organization’s subscription usage and agent-discard reports before changing sample rates. For a dedicated staging instance, both verification flags may remain 1: the server probe is bounded to process startup and the browser probe to one claim per exact release. A new release re-arms the browser probe. To retry one release after restoring Sentry capacity, remove only passport:sentry-verification:<release-sha> from browser local storage and reload. Keep both flags 0 in production.

Advanced tuning

These have sensible defaults and rarely need changing.
Health checks: GET /healthz returns {"ok":true} when the process is up. GET /readyz runs a cheap storage ping (SELECT 1 on Postgres), reports the active release and schemaVersion, and returns 503 if storage is unreachable. When Postgres is down, Passport fails closed on policy: mutating requests fail (and failed commits return 503 with retry guidance) rather than bypass controls. Some read-only paths may still serve a stale in-memory snapshot until connectivity returns.