Environment Variables
Configure secrets in one root .env, validated by the Zod schema in @repo/runtime.
A single root .env is the source of truth for local dev, validated at boot by the Zod schema in packages/runtime/src/env.ts. Set each value once; .env is gitignored and dev-only - production vars live on your deploy platform.
Required keys
Uncommented in .env.example. The @repo/runtime schema throws on boot if any of these is missing or invalid.
| Var | Purpose |
|---|---|
DATABASE_URL | Postgres connection string (validated by @repo/database) |
REDIS_URL | Cache + rate-limit store. Upstash builds require UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN instead |
BETTER_AUTH_SECRET | Min 32 chars, powers @repo/auth - openssl rand -base64 32 |
API_URL | Backend origin the runtime calls (must be a URL; falls back to the *_PUBLIC_API_URL keys) |
INNGEST_APP_ID / INNGEST_EVENT_KEY / INNGEST_BASE_URL | Background jobs (Background jobs) |
BASE_URL and TRUSTED_ORIGINS are schema-optional but ship uncommented - see Origins and base URL. Add INNGEST_SIGNING_KEY in production (read directly by Inngest, not schema-validated).
BETTER_AUTH_SECRET and all provider keys are server secrets - never sent to the browser or third parties. Generate tokens and QR codes client-side.
Feature keys
Optional - commented out in .env.example. Each stays inert until you both set its vars and flip the matching flag in @repo/config.
| Var(s) | Feature | When needed |
|---|---|---|
STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET | Payments | config.payment with provider: "stripe" |
POLAR_ACCESS_TOKEN / POLAR_WEBHOOK_SECRET | Payments | config.payment with provider: "polar" |
TURNSTILE_SECRET_KEY | CAPTCHA | config.captcha enabled |
CONTENT_API_KEY | Content API | config.contentApi enabled (endpoints return 503 if unset) |
GOOGLE_* / GITHUB_* (+ Facebook, Discord, Twitter) | Social OAuth | Provider listed in config.auth (Social OAuth) |
RESEND_API_KEY / AMAZON_SES_* / SMTP_* | config.email.provider (Email) | |
RESEND_AUDIENCE_ID / LISTMONK_* | Newsletter | config.newsletter.provider |
TWILIO_* / AMAZON_SNS_* | SMS | config.sms.provider (SMS) |
STORAGE_* | Storage | config.storage.provider: "s3" (Storage) |
DISCORD_WEBHOOK_URL / SLACK_WEBHOOK_URL / TELEGRAM_* | Admin alerts | config.adminNotifications (each provider auto-enables when its var is set) |
OPENROUTER_API_KEY | Translations (pnpm translate) + AI | Translating locales + packages/content; the pre-commit hook skips without it. No config flag. See i18n. |
Origins and base URL
These keys bridge env into @repo/config:
| Var | Backs | Notes |
|---|---|---|
BASE_URL | config.baseUrl (default https://generatesaas.com) | Frontend public origin - auth redirects, email links, CORS |
TRUSTED_ORIGINS | config.origins | Comma-separated allowlist; falls back to localhost dev ports if unset |
AUTH_COOKIE_DOMAIN | - | e.g. .example.com - cross-subdomain cookies for the separate-backend setup |
CORS and Better Auth read config.origins as-is - baseUrl is not auto-included. Your production origin must be in TRUSTED_ORIGINS or live logins redirect-fail.
How loaders work
Every app and tool reaches the same root .env, but by a different mechanism:
| Context | Mechanism |
|---|---|
| App dev scripts | dotenv -e .env -e ../../.env - app-local .env overrides root for conflicts |
| Standalone backend | node --env-file-if-exists ../../.env (dev uses tsx --env-file-if-exists) |
Drizzle / Better Auth CLI (@repo/database, e.g. auth:generate) | dotenv -e ../../.env |
| Turbo | .env listed in globalDotEnv so task caches bust on change |
| Tests | loaded explicitly in setup files |
The generated schema
The CLI emits packages/runtime/src/env.ts so feature code typechecks no matter which flags you toggle.
- Only the cache choice changes the required shape -
REDIS_URLvs. the Upstash pair are non-optional. - Everything else (social, payment, email, SMS, storage, captcha, admin alerts) is always emitted as
optional. - The schema validates once at module load and throws if a required key is missing or invalid.
- Enable a feature by setting its var and flipping the
@repo/configflag - never by editing this file.
Frequently asked questions
Why does my feature key do nothing?
A key alone is inert. The feature also needs its flag enabled in @repo/config - see Configuration.
Does .env ship to production?
No. It is gitignored and dev-only. Set vars on your deploy platform (project settings, Docker env) - see Deployment.
My production login redirects fail - why?
Your live origin is almost certainly missing from TRUSTED_ORIGINS. config.baseUrl is not auto-trusted; list the origin explicitly.