Configuration

July 10, 2026 · View on GitHub

Thunderbolt's backend is configured through environment variables. The schema lives at backend/src/config/settings.ts and is validated with Zod on startup — misconfiguration fails loud, not silent.

Copy the example to a .env file and customize:

cp backend/.env.example backend/.env

Variables marked required must be set before the backend will start.

Authentication

VariableDefaultRequiredDescription
AUTH_MODEconsumerconsumer for magic-link + Google/Microsoft OAuth, oidc for OIDC SSO, saml for SAML SSO
BETTER_AUTH_SECRETyesNon-empty string used to sign sessions. Generate with openssl rand -hex 32.
BETTER_AUTH_URLhttp://localhost:8000Public URL the backend is served at; used in OAuth redirects
GOOGLE_CLIENT_IDGoogle OAuth client ID (consumer mode)
GOOGLE_CLIENT_SECRETGoogle OAuth client secret
MICROSOFT_CLIENT_IDMicrosoft OAuth client ID
MICROSOFT_CLIENT_SECRETMicrosoft OAuth client secret
OIDC_ISSUEROIDC issuer URL (required when AUTH_MODE=oidc)
OIDC_DISCOVERY_URL${OIDC_ISSUER}/.well-known/openid-configurationOptional override for the OIDC discovery endpoint. Use when backend reaches the IdP at an internal hostname (e.g. http://keycloak:8080/...) but tokens are issued with a browser-facing hostname
OIDC_CLIENT_IDOIDC client ID
OIDC_CLIENT_SECRETOIDC client secret
SAML_ENTRY_POINTSAML IdP SSO URL (required when AUTH_MODE=saml)
SAML_ENTITY_IDSP entity ID — must match the SAML client ID in the IdP (e.g. thunderbolt-saml-sp)
SAML_IDP_ISSUERIdP entity ID / issuer (e.g. https://keycloak.example.com/realms/thunderbolt)
SAML_CERTSAML IdP signing certificate (base64, no PEM headers)

Consumer mode uses Better Auth's magic-link flow by default (email-delivered OTP). Hook up a provider by also setting the OAuth credentials above. Enterprise mode delegates entirely to an OIDC or SAML identity provider (Keycloak by default). See OIDC local dev and SAML local dev for setup guides.

Important: When using AUTH_MODE=oidc or saml, the IdP origin must be included in TRUSTED_ORIGINS (see CORS section below). The SSO plugin validates discovery/metadata URLs against this list.

AI Provider Keys

Set any subset; the app exposes each provider whose key is present.

VariableDescription
ANTHROPIC_API_KEYAnthropic (Claude)
FIREWORKS_API_KEYFireworks
MISTRAL_API_KEYMistral
EXA_API_KEYExa search (for web-grounded retrieval)
THUNDERBOLT_INFERENCE_URLCustom OpenAI-compatible inference endpoint
THUNDERBOLT_INFERENCE_API_KEYKey for the custom inference endpoint

User-level keys (e.g. OpenAI, OpenRouter) are configured in the app itself, not as backend env vars. For local inference, point THUNDERBOLT_INFERENCE_URL at an Ollama or llama.cpp server.

PowerSync

VariableDefaultRequiredDescription
POWERSYNC_URLyes (for sync)URL of the PowerSync service (e.g. http://localhost:8080 for local dev)
POWERSYNC_JWT_SECRETyes when URL setHS256 secret shared with PowerSync; must be ≥ 32 characters
POWERSYNC_JWT_KIDKey ID for PowerSync to pick among multiple secrets during rotation
POWERSYNC_TOKEN_EXPIRY_SECONDS3600PowerSync JWT lifetime

The JWT secret must match the k value the PowerSync service loads at runtime. For self-hosted deploys, deploy/config/powersync-config.yaml reads it from the PS_JWT_KEY_BASE64 env var (base64 of the raw secret); POWERSYNC_JWT_KID on the backend must match PS_JWT_KID set on the PowerSync service. For local dev, both values are baked into powersync-service/config/config.yaml.

CORS

VariableDefaultDescription
CORS_ORIGINShttp://localhost:1420,tauri://localhost,http://tauri.localhostExact-match allowed origins (comma-separated)
CORS_ALLOW_CREDENTIALStrueWhether browsers may send cookies
CORS_ALLOW_METHODSGET,POST,PUT,DELETE,PATCH,OPTIONSAllowed HTTP methods
CORS_ALLOW_HEADERS(see settings.ts)Allowed request headers. Add any new X-* header you introduce in the client.
CORS_EXPOSE_HEADERS(see settings.ts)Response headers exposed to the client

When you add a new custom header to a client request (e.g. X-Device-ID), you must add it to CORS_ALLOW_HEADERS — otherwise browser preflight fails and the request never reaches your handler.

Analytics

VariableDefaultDescription
POSTHOG_HOSThttps://us.i.posthog.comPostHog instance hostname
POSTHOG_API_KEYLeave unset to disable server-side analytics

See TELEMETRY.md in the repo for the full list of events the client emits.

Rate Limiting and Proxy Trust

VariableDefaultDescription
RATE_LIMIT_ENABLEDtrueSet to false to disable rate limiting (local dev only)
TRUSTED_PROXY""cloudflare trusts CF-Connecting-IP, akamai trusts True-Client-IP, empty trusts only the socket IP

Trusting the wrong proxy header lets a client spoof its IP for rate-limit bypass. Leave this empty unless you know your edge.

Waitlist

VariableDefaultDescription
WAITLIST_ENABLEDfalseFlip to true to require approval before new sign-ups can log in
WAITLIST_AUTO_APPROVE_DOMAINSComma-separated email domains that skip the waitlist queue

OpenTelemetry (Optional)

OpenTelemetry traces are enabled automatically when these are set. Not part of the Zod schema — the backend reads them from process.env directly.

VariableDescription
OTEL_EXPORTER_OTLP_ENDPOINTOTLP HTTP endpoint (e.g. http://localhost:4318/v1/traces)
OTEL_EXPORTER_OTLP_TOKENBearer token for authenticated collectors

Tested with BetterStack, Jaeger, Zipkin, New Relic, Grafana Cloud, and any OTLP-compatible collector.

General

VariableDefaultDescription
PORT8000HTTP port the backend listens on
APP_URLhttp://localhost:1420Public URL where the frontend is served
LOG_LEVELINFOOne of DEBUG, INFO, WARN, ERROR
SWAGGER_ENABLEDfalseExpose /v1/swagger with the full OpenAPI spec (don't in production)
MONITORING_TOKENShared secret for authenticated /health checks

Frontend Build Args

The web/desktop bundle accepts two Vite env vars, passed as Dockerfile build args in deploy/docker/frontend.Dockerfile:

ArgDefaultPurpose
VITE_THUNDERBOLT_CLOUD_URL/v1Backend API URL (relative path, proxied by nginx or ALB)
VITE_AUTH_MODEssoAuth mode — sso for enterprise SSO (OIDC or SAML), omit for consumer

Validating Your Config

The backend validates every variable on startup. Common hits:

  • BETTER_AUTH_SECRET: String must contain at least 1 character(s) — set it.
  • powersyncJwtSecret must be at least 32 characters when powersyncUrl is set — regenerate with openssl rand -hex 32.
  • AUTH_MODE: Invalid enum value — must be consumer, oidc, or saml (case-insensitive).