Skip to main content

Environment Variables

All pgStack configuration is done through environment variables. Copy .env.example to .env and fill in values before starting.

Generating secrets

# Use this for JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, POSTGRES_PASSWORD, SECRETS_ENCRYPTION_KEY
openssl rand -base64 32

Run it five times and use each output for a different secret.

Required variables

These must be set for production. In development, the compose file provides insecure defaults.

VariableExampleDescription
POSTGRES_PASSWORDXk8mP2nQ9r...PostgreSQL superuser password. Must be strong.
JWT_SECRETYm3vL5wK1t...Signing key for JWT access tokens. Must be 32+ characters.
ANON_KEYNz6xB4cJ8p...API key for anonymous (unauthenticated) requests.
SERVICE_ROLE_KEYQr1eD7hF3a...Admin API key. Bypasses RLS. Keep secret.
SITE_URLhttps://app.example.comPublic URL of your app. Required for OAuth redirects.
SECRETS_ENCRYPTION_KEYTn4vC9eB2k...32 bytes base64. AES-256-GCM key for every at-rest secret the proxy holds. Required whenever you use: (a) edge function env vars; (b) webhook signing secrets (pgstack.webhooks.secret_encrypted). With the key missing, CreateWebhook with a non-empty secret returns 400, and existing encrypted-secret rows are dropped from the in-memory delivery cache at refresh (fail-closed).

Database variables

VariableDefaultDescription
POSTGRES_DBappDatabase name.
POSTGRES_USERpostgresPostgreSQL superuser username. Used only to bootstrap the database — the proxy does not connect as this role.
AUTHENTICATOR_PASSWORD(required in prod)Login password for the powerless authenticator role the proxy connects as (not the superuser) — see [SECURITY.md invariant 22]. The db image's 20-authenticator-login.sh applies it at init; the proxy's DATABASE_URL uses it. Must be URL-safe. bootstrap-env.sh generates it; the production compose fails fast if it is unset.
DATABASE_URL(constructed)Full connection string, constructed by the compose file as postgres://authenticator:$AUTHENTICATOR_PASSWORD@db:5432/$POSTGRES_DB. The proxy connects as authenticator (NOSUPERUSER, NOINHERIT) and SET ROLEs per request, so a compromise cannot bypass RLS by default, run COPY … FROM PROGRAM, drop the database, or read pg_authid. Override only to point at an external PostgreSQL — keep the non-superuser role.

Proxy variables

VariableDefaultDescription
LISTEN_ADDR0.0.0.0:8080Address and port the proxy listens on.
PROXY_PORT8080Host port mapping (for docker compose).
ENABLE_SQL_ENDPOINTfalseEnable /api/sql endpoint for raw SQL (dev only). Never enable in production.
ENABLE_DEMO_ENDPOINTfalseMount POST /api/demo (mutates demo tables). Off by default; even when off the route is still mounted if a service_role key is configured (and then requires that key). Never enable on a public host.
PUBLIC_DIR(empty — disabled)Directory for serving static files at the root path. When set, files in this directory are served with SPA fallback (unknown paths serve index.html).
PUBLIC_ALLOW_INLINE_SCRIPTSfalseAdd 'unsafe-inline' to the script-src of the Content-Security-Policy sent with PUBLIC_DIR responses. Needed for single-file static apps that use inline <script> blocks. Default is strict (script-src 'self'); prefer external script files in production.
STUDIO_DIR/studioDirectory containing the Studio SPA build, served at /studio/ with a relaxed CSP. Point at your own build when customizing the image.
DOCS_DIR/docsDirectory containing the built documentation site (Docusaurus static output), served at /docs/.
PGSTACK_ENV(empty)Set to production to promote security misconfigurations (dev-only secrets, ENABLE_SQL_ENDPOINT=true, permissive CORS, REQUIRE_AUTHENTICATED_WS=false) from startup warnings to fatal startup errors. docker-compose.prod.yml sets it.
PROXY_RUST_PORT8081Host port for the legacy Rust reference proxy that the dev compose file starts alongside the primary Go proxy. Dev only — not part of the production stack.

Auth variables

VariableDefaultDescription
ACCESS_TOKEN_EXPIRY_SECS3600JWT access token lifetime (1 hour).
REFRESH_TOKEN_EXPIRY_DAYS30Refresh token lifetime (30 days).
AUTH_RATE_LIMIT30Auth endpoint rate limit (requests per window).
AUTH_RATE_WINDOW60Rate limit window in seconds.
REQUIRE_EMAIL_VERIFICATIONfalseBlock login for unverified emails. Requires SMTP to be configured.
ALLOW_ANONYMOUS_SIGNINfalseEnable POST /auth/v1/anonymous (SDK auth.signInAnonymously()), which mints a short-lived role=anon JWT without creating a user row. Default-deny: when false the endpoint returns 503.
ANONYMOUS_JWT_TTL3600Lifetime in seconds of anonymous JWTs. No refresh token is issued — clients must re-mint before expiry.
ANONYMOUS_RATE_LIMIT300Per-IP rate limit for /auth/v1/anonymous (requests per window). Defaults to 10 × AUTH_RATE_LIMIT; uses a limiter separate from the other auth endpoints.
ANONYMOUS_RATE_WINDOW60Anonymous sign-in rate limit window in seconds.

OAuth variables (optional)

Set these to enable social login. Each provider is independent — configure only the ones you need.

Google and GitHub

VariableWhere to get itDescription
GOOGLE_CLIENT_IDGoogle Cloud ConsoleGoogle OAuth App client ID.
GOOGLE_CLIENT_SECRETGoogle Cloud ConsoleGoogle OAuth App client secret.
GITHUB_CLIENT_IDGitHub SettingsGitHub OAuth App client ID.
GITHUB_CLIENT_SECRETGitHub SettingsGitHub OAuth App client secret.

Apple

VariableWhere to get itDescription
APPLE_CLIENT_IDApple DeveloperServices ID (reverse-domain, e.g. com.example.auth).
APPLE_TEAM_IDApple Developer account page (top-right)10-character Team ID.
APPLE_KEY_IDApple Developer > KeysKey ID for the Sign in with Apple private key.
APPLE_PRIVATE_KEYApple Developer > Keys (download .p8)ES256 private key contents. Used to generate the client secret JWT.

Microsoft

VariableWhere to get itDescription
MICROSOFT_CLIENT_IDAzure Portal > App registrationsApplication (client) ID.
MICROSOFT_CLIENT_SECRETAzure Portal > Certificates & secretsClient secret value.

Generic OIDC

VariableWhere to get itDescription
OIDC_CLIENT_IDYour OIDC provider dashboardClient ID from any OpenID Connect provider.
OIDC_CLIENT_SECRETYour OIDC provider dashboardClient secret.
OIDC_ISSUER_URLYour OIDC providerIssuer URL (e.g. https://auth.example.com). Must serve /.well-known/openid-configuration.

Redirect URI to register with OAuth providers: https://your-app.example.com/auth/v1/callback

SMTP variables (optional)

Required for email verification and password reset emails. If not set, these features are disabled.

VariableDefaultDescription
SMTP_HOSTSMTP server hostname. Example: smtp.sendgrid.net
SMTP_PORT587SMTP port. Use 587 for STARTTLS, 465 for SSL, 25 for unencrypted.
SMTP_USERSMTP authentication username.
SMTP_PASSSMTP authentication password.
SMTP_FROMnoreply@pgstack.localFrom address for all auth emails.
ALLOW_INSECURE_SMTPfalseDev opt-out for the production TLS policy. Under PGSTACK_ENV=production the mailer refuses to send over an un-encrypted connection on any port (implicit TLS on 465, STARTTLS elsewhere) so AUTH credentials and reset/magic-link tokens never cross the wire in clear. Set true only for a trusted private-network relay.

CORS / Origin variables

VariableDefaultDescription
ALLOWED_ORIGINS(empty)Comma-separated list of allowed CORS / WebSocket origins. For production set explicitly: https://app.example.com,https://admin.example.com.
ALLOW_PERMISSIVE_CORSfalseWhen ALLOWED_ORIGINS is empty AND this is true, the proxy reflects any Origin (or * when no Origin header is present) without Access-Control-Allow-Credentials — reflected Origin + credentials is the CSRF-amplifying combo the security audit removed, and Bearer/apikey header auth does not need it. Default is strict: no Allow-Origin header is emitted, cross-origin browsers are blocked, same-origin and non-browser clients still work. The proxy logs a loud WARN when this is enabled. Never enable on a public host.

The strict default (since the 2026-05 security audit) prevents a CSRF/exfiltration footgun where any site could call your API with the user's credentials. Pre-audit deployments that relied on the permissive default must either set ALLOWED_ORIGINS or ALLOW_PERMISSIVE_CORS=true for dev.

Reverse proxy variables

VariableDefaultDescription
TRUSTED_PROXIES(empty)Comma-separated list of trusted reverse proxy IPs. When empty, X-Forwarded-For is ignored and rate limiting uses RemoteAddr directly. Set this when running behind a load balancer or reverse proxy (e.g., 10.0.0.1,172.17.0.1).

Edge Functions variables

VariableDefaultDescription
EDGE_RUNTIME_URL(empty — disabled)URL of Supabase edge-runtime container. Example: http://edge-runtime:9000. Empty disables Deno backend.
EMBEDDED_FUNCTIONStrueEnable embedded JS runtime (goja) for lightweight functions.
EDGE_FN_TIMEOUT_MS60000Per-invocation deadline (ms) applied to every edge function runtime (goja, plv8, plpython, Deno, http).

Batch transaction variables

VariableDefaultDescription
MAX_BATCH_OPERATIONS20Maximum operations per POST /rest/v1/batch request.

Realtime / WebSocket variables

VariableDefaultDescription
MAX_CONNECTIONS10000Maximum concurrent WebSocket connections. Enforced as a semaphore on WebSocket upgrades; notifications are fanned out from a single dedicated PostgreSQL LISTEN connection, so WebSocket clients do not hold PG sessions.
REQUIRE_AUTHENTICATED_WSfalseWhen true, anon-key WS subscriptions are rejected — only real JWTs (authenticated or service_role) can open /ws/{query_id}. Closes the "public anon key + guessable query_id" read vector. Required under PGSTACK_ENV=production: the proxy refuses to start with false when JWT_SECRET and ANON_KEY are set, and docker-compose.prod.yml pins it to true.
WS_MAX_PER_IP10Maximum concurrent WebSocket connections per client IP (0 disables the cap). Excess connections are rejected with 429 before the upgrade completes. Honours TRUSTED_PROXIES when resolving the client IP, like the auth rate limiter. Raise this when many users share a NAT or corporate proxy IP.
BROADCAST_CAPACITY1024Per-client buffered message capacity for NOTIFY fan-out. A client that falls further behind is disconnected rather than blocking the hub.

Connection / body size limits

VariableDefaultDescription
MAX_BODY_SIZE1048576Maximum request body size in bytes (default: 1 MB).
MAX_FILE_SIZE52428800Maximum storage upload size in bytes (default: 50 MB).

Storage

VariableDefaultDescription
STORAGE_BACKENDdatabaseStorage backend for file uploads: database (bytea column) or disk (filesystem).
STORAGE_DIR/data/storageFilesystem path for uploaded files when STORAGE_BACKEND=disk. Mount as a Docker volume for persistence.

HTTP Server

VariableDefaultDescription
READ_TIMEOUT30HTTP read timeout in seconds
WRITE_TIMEOUT60HTTP write timeout in seconds
IDLE_TIMEOUT120HTTP keep-alive idle timeout in seconds
SHUTDOWN_TIMEOUT15Graceful shutdown deadline in seconds
GZIP_LEVEL5Gzip compression level (0=disabled, 1=fastest, 9=best)

WebSocket connections may need longer WRITE_TIMEOUT values. The default 60s is sufficient for most use cases.

Logging

VariableDefaultDescription
LOG_LEVELinfoLog verbosity: trace, debug, info, warn, error, fatal, panic.
LOG_PRETTY(unset)Set to 1 for human-readable console output instead of structured JSON.

PostgreSQL Connection Pool

VariableDefaultDescription
PG_POOL_MAX_CONNS50Maximum connections in the proxy's PG pool
PG_POOL_MIN_CONNS5Minimum idle connections kept warm
PG_POOL_MAX_CONN_IDLE_TIME300Seconds before an idle connection is closed
PG_RECONNECT_MAX_BACKOFF30Maximum backoff in seconds between reconnect attempts of the dedicated LISTEN connection

This is the proxy's built-in pool. For advanced pooling (transaction mode, prepared statements), use PgBouncer in front of PostgreSQL.

TLS / HTTPS

VariableDefaultDescription
TLS_MODEoffTLS mode: off (use reverse proxy), auto (Let's Encrypt), manual (own certs)
TLS_DOMAINSComma-separated domains for auto mode (required when TLS_MODE=auto)
TLS_CERT_DIR/data/certsDirectory to cache Let's Encrypt certificates
TLS_ACME_EMAILEmail for Let's Encrypt expiry notifications (optional)
TLS_CERT_FILEPath to TLS certificate (required when TLS_MODE=manual)
TLS_KEY_FILEPath to TLS private key (required when TLS_MODE=manual)
HTTPS_ADDR:443Listen address for HTTPS (ignored when TLS_MODE=off)
HTTP_ADDR:80Listen address for ACME challenges + HTTP→HTTPS redirect
HSTS_MAX_AGE_SECONDS63072000Strict-Transport-Security max-age (2 years — the hstspreload.org minimum). Sent only on requests that demonstrably arrived over TLS: direct TLS, or X-Forwarded-Proto: https from a TRUSTED_PROXIES address. Lower it during initial TLS rollout to limit blast radius if TLS breaks.
HSTS_PRELOADfalseAppend preload to the HSTS header. Opt-in only: preload-list submission is effectively irreversible, so enable it only once you are committed to HTTPS-only.

In auto mode the proxy handles Let's Encrypt certificate issuance and renewal automatically. Ports 80 and 443 must be publicly reachable. For most deployments behind a load balancer or CDN, use TLS_MODE=off with a reverse proxy (Caddy, nginx) handling TLS termination.

PostgreSQL tuning (via docker-compose command flags)

These are set as PostgreSQL GUC parameters via the command: field in docker-compose.prod.yml, not as environment variables:

ParameterDefaultDescription
pg_reactive.max_subscriptions1024Maximum live query subscriptions (shared memory slots). Requires restart.
max_connections200PostgreSQL max client connections.
shared_buffers256MBPostgreSQL shared memory buffer. Set to 25% of RAM.
effective_cache_size768MBEstimated effective cache size. Set to 75% of RAM.

Example in docker-compose.prod.yml:

command: >
postgres
-c shared_preload_libraries=pg_reactive
-c pg_reactive.max_subscriptions=4096
-c max_connections=300
-c shared_buffers=512MB
-c effective_cache_size=1536MB
-c log_min_messages=warning
-c log_statement=none

Environment variable precedence

  1. Variables explicitly set in the shell environment
  2. Variables in .env file (loaded by docker compose)
  3. Default values in docker-compose.yml

Security checklist

Before going to production:

  • POSTGRES_PASSWORD is a strong random value (not the .env.example default)
  • JWT_SECRET is a strong random value (32+ characters)
  • ANON_KEY is a strong random value
  • SERVICE_ROLE_KEY is a strong random value, treated like a database password
  • ENABLE_SQL_ENDPOINT is false or unset
  • ENABLE_DEMO_ENDPOINT is false or unset
  • ALLOWED_ORIGINS is set to your specific domain(s); ALLOW_PERMISSIVE_CORS is false or unset
  • REQUIRE_AUTHENTICATED_WS is true whenever realtime queries return user-scoped data
  • SITE_URL is set to your production URL
  • .env is in .gitignore (never committed)
  • PostgreSQL port (5432) is not exposed to the internet
  • TRUSTED_PROXIES is set if running behind a reverse proxy (prevents rate limit bypass via spoofed X-Forwarded-For headers)
  • JWT_SECRET does not contain dev-only (the proxy warns on startup if it does, and refuses to start entirely when PGSTACK_ENV=production)
  • REQUIRE_EMAIL_VERIFICATION is true if SMTP is configured