Environment variables
All variables are validated at startup using Zod. If a required variable is missing or invalid, the process exits with an error listing every failing field.
Server
| Variable | Required | Default | Description |
|---|---|---|---|
PORT | no | 3001 | TCP port the Fastify server binds to |
HOST | no | 0.0.0.0 | Bind address |
NODE_ENV | no | development | development, production, or test. Migrations run at startup whenever it is not development. In production, e-mail verification is required by default (the Compose file overrides this default with false, see REQUIRE_EMAIL_VERIFICATION) and a failed SMTP check at startup is fatal. The Compose stack forces production. |
LOG_LEVEL | no | debug in development, info otherwise | pino log level (trace, debug, info, warn, error, fatal) |
BetterAuth (required)
| Variable | Required | Default | Description |
|---|---|---|---|
BETTER_AUTH_SECRET | yes | — | Random secret used to sign sessions (min 16 chars). Generate with openssl rand -base64 32. |
BETTER_AUTH_URL | yes | — | Public base URL of Auth Service, e.g. https://auth.example.com. Used as the OIDC issuer and in email links. |
Application identity
| Variable | Required | Default | Description |
|---|---|---|---|
APP_NAME | no | CIRCLE Auth | Display name used as the TOTP issuer in authenticator apps and as the BetterAuth appName. |
APP_LOGO_URL | no | — | URL of a logo displayed in the UI (sidebar, login, consent). When unset, the bundled CIRCLE mark (an inline SVG that follows the theme) is used. Without APP_LOGO_DARK_URL, the UI adapts the logo to the active theme (inverts a black or white logo, adds a contrasting circle behind a coloured one; needs the image host to send CORS headers). |
APP_LOGO_DARK_URL | no | — | URL of the logo used on the dark theme. Requires APP_LOGO_URL, which is then used on the light theme (and in e-mails). Both are displayed as supplied: no inversion or background is applied. |
SHOWCASE | no | false | Serves a public landing page at / presenting the service's features, with the instance logo. When false, / goes straight to the sign-in. The administration sign-in is at /auth in both cases; a bare /login (no OAuth parameters) leads to the landing page when SHOWCASE=true. |
Database (required)
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL | yes | — | PostgreSQL connection string, e.g. postgres://user:pass@host:5432/dbname |
POSTGRES_USER | no | auth | Postgres user — used by the postgres service and the DATABASE_URL of the Compose file |
POSTGRES_PASSWORD | no | auth | Postgres password. Change it for any shared or production deployment |
POSTGRES_DB | no | auth | Database name |
POSTGRES_PORT | no | 5433 | Host-side port published by the Postgres container |
Bootstrap
| Variable | Required | Default | Description |
|---|---|---|---|
ADMIN_EMAIL | no | — | Email address for the superadmin created at first boot |
ADMIN_PASSWORD | no | — | Password for the superadmin (min 12 chars; in production the placeholder from .env.example and other known default passwords are refused and no superadmin is created). If either variable is absent, bootstrap is skipped. |
CORS and sessions
| Variable | Required | Default | Description |
|---|---|---|---|
CORS_ORIGINS | no | http://localhost:5173 | Comma-separated list of allowed CORS origins |
SESSION_DOMAIN | no | — | When set, enables cross-subdomain cookies. Set to the apex domain, e.g. example.com. |
TRUST_PROXY_HOPS | no | 0 | Number of trusted reverse-proxy hops in front of the service. Passed to Fastify's trustProxy so req.ip is derived from the correct entry in the X-Forwarded-For chain instead of the spoofable client-supplied value. Defaults to 0 (secure): set it to 1 when running behind a single reverse proxy, otherwise req.ip is the proxy address and per-IP rate limiting collapses into one bucket. |
Redis (optional)
| Variable | Required | Default | Description |
|---|---|---|---|
REDIS_URL | no | — | Shared state across instances. When set, the rate-limit buckets and the admin real-time event bus are shared through Redis (ioredis is loaded lazily); when unset, both are in-process and therefore per-instance. Compose ships a redis service behind the redis profile. |
Email (SMTP)
SMTP_HOST is optional only while no e-mail flow is active. In production, e-mail verification is required by default, so the process refuses to start without SMTP_HOST unless REQUIRE_EMAIL_VERIFICATION=false and neither MAGIC_LINK_ENABLED nor EMAIL_OTP_ENABLED is on. The bundled Compose file passes REQUIRE_EMAIL_VERIFICATION=false when the variable is not set in .env, so that stack starts without SMTP until you enable it. When SMTP_HOST is unset, e-mails are dropped with a log line.
| Variable | Required | Default | Description |
|---|---|---|---|
SMTP_HOST | conditional | — | SMTP server hostname |
SMTP_PORT | no | 587 | SMTP port. Port 465 enables implicit TLS; all others use STARTTLS. |
SMTP_USER | no | — | SMTP authentication username. Authentication is used only when both SMTP_USER and SMTP_PASS are set |
SMTP_PASS | no | — | SMTP authentication password |
SMTP_FROM | no | auth-service <no-reply@localhost> | From address for outgoing emails |
MAIL_REPLY_TO | no | — | Reply-To header added to every outgoing e-mail |
REQUIRE_EMAIL_VERIFICATION | no | true in production, false otherwise | Refuse to issue a session until the e-mail address is verified. The Compose file and .env.example both set false, so the Compose stack does not require verification unless you set true |
MAGIC_LINK_ENABLED | no | false | Passwordless sign-in by e-mailed link, for existing accounts only. Needs SMTP and rate limits in place |
EMAIL_OTP_ENABLED | no | false | Passwordless sign-in by e-mailed one-time code, for existing accounts only |
Boolean flags accept true/false, 1/0, yes/no and on/off; any other value stops the startup.
Optional security features
| Variable | Required | Default | Description |
|---|---|---|---|
HAVEIBEENPWNED_ENABLED | no | false | Reject passwords found in the Have I Been Pwned corpus (k-anonymity: only a 5-character hash prefix leaves the process). Adds an outbound call on every password write |
LAST_LOGIN_METHOD_ENABLED | no | true | Set a cookie remembering the last sign-in method. The server-rendered sign-in page (see TEMPLATES_DIR) reads it to offer only the social provider a returning user last used. |
DEVICE_AUTHORIZATION_ENABLED | no | false | OAuth 2.0 Device Authorization Grant (RFC 8628) and the /device approval page |
CAPTCHA_ENABLED | no | false | Require a CAPTCHA token (x-captcha-response header) on sign-up, sign-in and password reset. Needs the three variables below |
CAPTCHA_PROVIDER | with CAPTCHA | — | cloudflare-turnstile, google-recaptcha, hcaptcha or captchafox |
CAPTCHA_SECRET_KEY | with CAPTCHA | — | Provider secret key |
CAPTCHA_SITE_KEY | with CAPTCHA | — | Public site key of the provider. It is not exposed to clients by the service: neither the admin SPA nor the built-in templates render a CAPTCHA widget |
CAPTCHA_ENDPOINTS | no | sign-up, sign-in, password reset | Comma-separated paths relative to /api/auth (for example /sign-up/email) |
A half-configured CAPTCHA stops the startup rather than locking sign-in out. Enable it only once your clients submit the token: the service ships no CAPTCHA widget yet, so with CAPTCHA_ENABLED=true the protected endpoints reject every request that lacks the x-captcha-response header.
Templates
| Variable | Required | Default | Description |
|---|---|---|---|
TEMPLATES_DIR | no | — | Absolute path to a directory containing custom templates for the server-rendered login, register, verify-email, email-verified, select-org, two-factor, device and error pages (plus the invitation and billing pages) and for the transactional emails (emails/*.eml). When unset, the pages listed first are served by the admin SPA and every e-mail uses the built-in template. Only the files you override are needed: built-in templates ship in the image and are used as a fallback. The bundled docker-compose.yml passes the variable through and mounts ./templates at /templates (read-only): set TEMPLATES_DIR=/templates to use it, or mount your own directory. See Custom templates. |
OAuth 2.1 resource audiences
| Variable | Required | Default | Description |
|---|---|---|---|
| (removed) | — | — | Replaced by per-application protected resources (oauth_resource). A client obtains a JWT access token by sending resource=<application url>. |
Stripe billing
| Variable | Required | Default | Description |
|---|---|---|---|
STRIPE_SECRET_KEY | no | — | Stripe secret API key (sk_live_… or sk_test_…). When absent, Stripe features are disabled. |
STRIPE_WEBHOOK_SECRET | no | — | Stripe webhook signing secret (whsec_…). Required for the webhook endpoint to validate Stripe events; without it the endpoint answers 503 (fail-closed). |
WALLET_TOPUP_MIN_EUR | no | 5 | Smallest online credit top-up, in whole euros |
WALLET_TOPUP_MAX_EUR | no | 500 | Largest online credit top-up, in whole euros. Must be at least the minimum |
WALLET_USD_TO_EUR_RATE | no | — | Euros per US dollar (decimal, e.g. 0.92) used to price LLM usage. Starting value only: once a superadmin saves the pricing in the admin interface, the saved values apply. Deliberately without default: with neither, dollar-priced usage answers 503 WAL_007 |
WALLET_MARGIN_PERCENT | no | 0 | Starting value of the percentage added to the provider cost (0–1000); see WALLET_USD_TO_EUR_RATE |
Both Stripe values are environment-only and are never returned by the admin API. Audit and repair with pnpm billing:reconcile --check (read-only) and pnpm billing:reconcile; --recreate-missing recreates a confirmed-deleted subscription, and --cancel-superseded cancels a replaced one only when the replacement is live. See the Stripe integration guide.
Compatibility with another identity provider
One profile for the whole instance; see Compatibility profiles. The profile only supplies defaults: every variable below overrides it, and custom starts from the plain defaults. An invalid value stops the startup.
| Variable | Required | Default | Description |
|---|---|---|---|
COMPAT_PROFILE | no | default | default, okta, auth0, keycloak or custom |
TOKEN_ENDPOINT_AUTH_METHOD | no | from profile (basic; post for Auth0) | How confidential applications authenticate at the token endpoint: basic (HTTP Basic) or post (client_secret in the body). A client accepts one method only. Applied to new applications and, at startup, to existing non-system ones |
JWT_PRIMARY_ALG | no | from profile (EdDSA; RS256 for Okta, Auth0, Keycloak) | Signature of access and ID tokens. Choose it before the first start: changing it later creates a key of the new algorithm at startup and logs a warning |
DEFAULT_AUDIENCE | no | from profile (true for Okta, Keycloak) | Issue a JWT even when the request carries no resource, using the application's url |
AUDIENCE_PARAM | no | from profile (true for Auth0) | Accept audience as an alias of resource |
CLAIM_MAPPINGS | no | from profile | JSON array of { "claim", "source", "scope"? }, source being roles, permissions, scope, email or clientId. Replaces the profile's list |
COMPAT_ALIASES | no | from profile | Comma-separated: okta, auth0. Endpoint paths of that provider answered by this service |
LiteLLM gateway (optional)
| Variable | Required | Default | Description |
|---|---|---|---|
LITELLM_URL | no | — | Base URL of the LiteLLM administration API, used to keep one gateway team per subscription plan of every application with LiteLLM support. The Compose file sets http://litellm:4000 |
LITELLM_PROVISIONER_KEY_FILE | no | /run/litellm-provisioner/key | File holding the restricted virtual key written by litellm-provision |
See LiteLLM gateway for the full setup.
Social login providers
Each provider requires both CLIENT_ID and CLIENT_SECRET to be set. If either is missing, the provider is disabled.
| Variable | Required | Default | Description |
|---|---|---|---|
GOOGLE_CLIENT_ID | no | — | Google OAuth app client ID |
GOOGLE_CLIENT_SECRET | no | — | Google OAuth app client secret |
GITHUB_CLIENT_ID | no | — | GitHub OAuth app client ID |
GITHUB_CLIENT_SECRET | no | — | GitHub OAuth app client secret |
LINKEDIN_CLIENT_ID | no | — | LinkedIn OAuth app client ID |
LINKEDIN_CLIENT_SECRET | no | — | LinkedIn OAuth app client secret |
MICROSOFT_CLIENT_ID | no | — | Microsoft OAuth app client ID |
MICROSOFT_CLIENT_SECRET | no | — | Microsoft OAuth app client secret |
APPLE_CLIENT_ID | no | — | Apple OAuth app client ID |
APPLE_CLIENT_SECRET | no | — | Apple OAuth app client secret |
Every provider key is validated at startup, but only Google and GitHub are wired into BetterAuth today. The LinkedIn, Microsoft and Apple variables are accepted and shown in the admin configuration page, yet they do not enable a sign-in button.