Node & Postgres (Docker Compose)
| Variable | Purpose |
|---|
| NODE_ENV | development | production | test. |
| ENABLE_SWAGGER_IN_PROD | Swagger UI at /api/docs is disabled by default once NODE_ENV=production (it’s reconnaissance value for an attacker, not something end users need). Set true to explicitly re-enable it. |
| POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB | Only used by docker-compose.yml to provision the local Postgres container — not read by the app itself once DATABASE_URL points at a real database. |
Database
| Variable | Purpose |
|---|
| DATABASE_URLrequired | PostgreSQL connection string for Prisma. |
JWT & session secrets
Symmetric secrets (openssl rand -base64 32), not keypairs. Access and refresh tokens are signed with separate secrets on purpose — a leaked refresh secret alone can’t forge access tokens.
| Variable | Purpose |
|---|
| JWT_SECRETrequired | Signs and verifies access tokens. Minimum 32 characters — the API refuses to start with a shorter one or a leftover placeholder value. |
| JWT_REFRESH_SECRETrequired | Signs and verifies refresh tokens. Same 32-character minimum. |
| JWT_ACCESS_EXPIRY | Access token lifetime. Default 15m. |
| JWT_REFRESH_EXPIRY | Refresh token lifetime. Default 7d. |
| COOKIE_SECRETrequired | Signs the httpOnly refresh-token cookie. |
| ENCRYPTION_KEYrequired | AES key for field-level PII encryption at rest (emails, phone numbers). |
| AUDIT_LOG_HMAC_SECRET | HMAC key for audit-log tamper-evidence (audit-logs module, ENTERPRISE tier). Falls back to JWT_SECRET if unset so logging never breaks — but that means a leaked JWT secret also compromises audit-log integrity, not just token forgery. Worth setting separately. |
| AUDIT_LOG_RETENTION_DAYS | How long audit log entries are kept before the purge scheduler deletes them. Defaults to 365 days. |
Auth policy
All optional — sane defaults ship without setting any of these.
| Variable | Purpose |
|---|
| AUTH_MAX_LOGIN_ATTEMPTS | Failed logins before account lockout. Default 5. |
| AUTH_LOCKOUT_MINUTES | Lockout duration once the attempt limit is hit. Default 15. |
| AUTH_EMAIL_VERIFICATION_EXPIRY_HOURS | Default 24. |
| AUTH_PASSWORD_RESET_EXPIRY_HOURS | Default 24. |
| AUTH_RECENT_AUTH_WINDOW_MINUTES | How recently a user must have actually logged in (not merely refreshed a token) before a step-up action — changing their password, deleting their account — is allowed. Default 15. Enforced by RecentAuthGuard. |
API server & reverse proxy
| Variable | Purpose |
|---|
| PORT | API listen port. 12002 in every shipped config. |
| TRUST_PROXY_ADDRESSES | Comma-separated trusted reverse-proxy IP(s)/CIDR range(s) (nginx-ingress, ALB, Cloudflare). Leave unset with no proxy in front. Only set this behind a proxy that provably strips/overwrites inbound X-Forwarded-For before appending its own hop — otherwise a client can forge that header to get a fresh, never-throttled rate-limit bucket on every request (a real, confirmed exploit path against /auth/login without this set correctly). Per-account brute-force lockout is unaffected either way, since that’s tracked by user row, not IP. |
Frontend & CORS
| Variable | Purpose |
|---|
| NEXT_PUBLIC_API_URLrequired | API origin the browser calls directly. Inlined into the Next.js client bundle at BUILD time — must be a Docker build-arg, not a runtime env var, when building the web image. |
| FRONTEND_URLrequired | The canonical web app origin — used by the API for CORS, email links, and OAuth/checkout redirect allowlisting. |
| CORS_ADDITIONAL_ORIGINS | Optional comma-separated origins to allow via CORS beyond FRONTEND_URL (a staging environment, or an apex + www split). Only affects CORS — email links and redirects always use FRONTEND_URL. |
| API_URL | The API’s own public URL, read live at runtime by the API process itself — used to build absolute callback URLs it constructs server-side (currently: the SAML SSO ACS callback in the mfa module). Distinct from NEXT_PUBLIC_API_URL, which is inlined into the browser bundle. Required if SSO is enabled and a provider doesn’t set its own callback URL — left unset, the callback silently becomes the literal string "undefined/mfa/sso/acs" instead of failing loudly. |
Valkey
| Variable | Purpose |
|---|
| REDIS_URLrequired | Valkey (Redis-protocol-compatible) connection string — caching, queues, rate-limit buckets. |
Email (Resend)
| Variable | Purpose |
|---|
| RESEND_API_KEY | Required for email features — verification, password reset, team invitations. |
| FROM_EMAIL | Sender address for all transactional email. |
Billing — Stripe
| Variable | Purpose |
|---|
| STRIPE_SECRET_KEY | Required for billing features. |
| STRIPE_PUBLISHABLE_KEY | Used server-side. |
| STRIPE_WEBHOOK_SECRET | Verifies the Stripe webhook signature. |
| NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | Same publishable key, set again for the web app — it’s a separate deployment target and can’t read apps/api’s .env. Safe to expose to the browser: this key can only create tokens/PaymentMethods, never charge anything on its own. Inlined into the client bundle at build time. |
Billing — Razorpay (India)
| Variable | Purpose |
|---|
| RAZORPAY_KEY_ID | Required for the Indian payment gateway. |
| RAZORPAY_KEY_SECRET | Razorpay API secret. |
| RAZORPAY_WEBHOOK_SECRET | Verifies the Razorpay webhook signature (HMAC-SHA256). |
OAuth providers (PRO tier)
| Variable | Purpose |
|---|
| GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET / GOOGLE_CALLBACK_URL | Google OAuth app credentials and callback URL. |
| GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET / GITHUB_CALLBACK_URL | GitHub OAuth app credentials and callback URL. |
| MICROSOFT_CLIENT_ID / MICROSOFT_CLIENT_SECRET / MICROSOFT_CALLBACK_URL | Microsoft OAuth app credentials and callback URL. |
MFA (ENTERPRISE tier)
| Variable | Purpose |
|---|
| WEBAUTHN_RP_ID | Must exactly match your public domain (no protocol, no port). Defaults to localhost — fine for local dev, wrong for anything else. A mismatch fails passkey registration and login without an obvious error. |
| WEBAUTHN_ORIGIN | Must exactly match the full origin users access the app from. Defaults to http://localhost:12003. |
| TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN / TWILIO_PHONE_NUMBER | SMS-based MFA delivery via Twilio. |
Webhooks & licensing
| Variable | Purpose |
|---|
| WEBHOOK_DELIVERY_RETENTION_DAYS | How long outbound webhook delivery logs are kept before a scheduled purge (data minimization). Default 90; 0 disables the purge. |
| LICENSE_SERVER_URL / LICENSE_SECRET | License key validation, if your deployment uses one. |
Observability
| Variable | Purpose |
|---|
| LOG_LEVEL | error | warn | info | debug | verbose. |
| SENTRY_DSN | Backend error tracking (optional). |
| NEXT_PUBLIC_SENTRY_DSN | Frontend error tracking — public DSN, safe to expose to the browser. |
| SENTRY_ORG / SENTRY_PROJECT_API / SENTRY_PROJECT_WEB | Source-map upload targets — set as CI variables, not local env. |
Analytics & marketing (optional, frontend)
All NEXT_PUBLIC_ — exposed to the browser by design.
| Variable | Purpose |
|---|
| NEXT_PUBLIC_GA_MEASUREMENT_ID | Google Analytics. |
| NEXT_PUBLIC_FACEBOOK_PIXEL_ID | Meta Pixel. |
| NEXT_PUBLIC_GOOGLE_ADS_ID | Google Ads conversion tracking. |
| NEXT_PUBLIC_LINKEDIN_PARTNER_ID | LinkedIn Insight Tag. |
Generate the four random secrets before your first deploy — the exact command is on the
Deployment page. The API refuses to start if a critical secret is missing, still contains a placeholder string, or is under 32 characters.