Skip to content

Production deployment

Last Updated: 2026-06-11

This page describes how to run Substratum’s gateway and file-explorer UI in production. It mirrors the local Compose edge pattern (one public origin for UI, API, and OAuth metadata), but with HTTPS, static assets, and managed services instead of Vite dev server.

Design goal: one browser origin

The browser, OAuth redirect URI, session cookie, and discoverable client metadata must all use the same public origin — the value of PUBLIC_BASE_URL (no trailing slash).

Request routing

Match docker/nginx-edge.conf in production:

PathHandlerNotes
/Static SPA (index.html, JS assets)Build with nx run file-explorer:build; configure SPA fallback for client routes
/api/*GatewayInclude X-Forwarded-Proto, Host, client IP headers
/.well-known/*GatewayRequired for OAuth client_id document

Release artifacts

ComponentArtifactNotes
GatewayDockerfile.gateway runtime stage → substratum-gateway binaryRuns as nobody; bind HOST/PORT (default 0.0.0.0:18080 internally)
File explorerdist/apps/file-explorer/ from Nx production buildServed by CDN/nginx, not embedded in gateway today
DatabaseManaged PostgreSQLProvision infra/data; bootstrap via data deployment; app pool uses substratum_gateway (RLS)
BlocksS3-compatible storage or FlatFSSet S3_* in production; FlatFS is for small/self-host only

Required configuration

Set via environment or substratum-gateway.json (env wins on conflict):

VariableProduction notes
PUBLIC_BASE_URLhttps://app.example.com — must match TLS cert and the URL users open
DATABASE_URLApp role URL from infra/data stack output databaseUri — gateway, ops-api, pds-authz-proxy runtime
DATABASE_ADMIN_URL**doadmin admin URI from infra/data stack output databaseAdminUri — migrations/bootstrap only, not gateway runtime
JWT_SECRETStrong random secret; no debug fallback in production
SWARM_MASTER_SECRETRequired in production
LIBP2P_IDENTITY_KEYStable base64 libp2p keypair
CORS_ALLOWED_ORIGINSSame origin as SPA if UI and API share PUBLIC_BASE_URL; add extra origins only for separate admin UIs
HOST / PORTGateway listen address behind the proxy

Optional PDS-related vars apply when you operate a Substratum-hosted PDS or signup proxy — see OAuth and PDS origins. For sign-in against public networks (Bluesky handles), users’ home PDSes are resolved at OAuth time; you do not run a local PDS container.

Pre-deploy checklist

  1. DNS + TLS — Certificate covers the exact host in PUBLIC_BASE_URL (pick apex or www, redirect the other).
  2. Build UIpnpm install then nx run file-explorer:build; upload dist/apps/file-explorer to static hosting or mount in nginx.
  3. Build gatewaydocker build -f Dockerfile.gateway --target runtime -t substratum-gateway .
  4. Migrationssea-orm-cli migrate up with DATABASE_ADMIN_URL (one-off deploy step, not on gateway startup).
  5. Smoke tests
    • GET https://app.example.com/.well-known/oauth-client-metadata.json returns JSON; client_id matches that URL.
    • GET https://app.example.com/api/v1/health (or drives with 401) reaches gateway through proxy.
    • Complete OAuth login; confirm substratum_session cookie on PUBLIC_BASE_URL origin with HttpOnly and Secure (when PUBLIC_BASE_URL is https://).
  6. Secrets — No .env in the image; inject via platform secret store (ADR 13).

Differences from local Compose

Local devProduction
Vite on :14200 (Compose-internal)Static dist/
nginx edge on http://127.0.0.1:8080HTTPS reverse proxy / CDN
Loopback OAuth client metadataDiscoverable HTTPS metadata
Optional local PDS on :3000Users’ federated PDSes
Devcontainer forwards 8080 onlyPublic URL is the only entry point

See also