Environment Variables
The required keys, the origin and rate-limit knobs, how every tool finds the root .env, and the generated Zod schema that validates it at boot.
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. It is gitignored and dev-only - production vars live on your deploy platform.
Required keys
Uncommented in .env.example. The schema throws on boot if any of these is missing or invalid.
| Var | Purpose |
|---|---|
DATABASE_URL | Postgres connection string |
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 -hex 32 |
API_URL | Backend origin the runtime calls. Fullstack builds derive it from NEXT_PUBLIC_API_URL; a standalone backend sets it |
INNGEST_APP_ID / INNGEST_EVENT_KEY / INNGEST_BASE_URL | Background jobs - see Background jobs |
Also shipped uncommented: BASE_URL. Add INNGEST_SIGNING_KEY in production (schema-optional; the Inngest SDK reads it from the environment itself).
BETTER_AUTH_SECRET and all provider keys are server secrets - never send them to the browser or a third party. Generate tokens and QR codes client-side.
Origins and base URL
| Var | Backs | Notes |
|---|---|---|
BASE_URL | config.baseUrl | The public origin. Server code prefers it over the committed literal for auth redirects, email links, device verification and checkout returns |
TRUSTED_ORIGINS | config.origins | Comma-separated. Unioned with the static config.origins, not a replacement |
AUTH_COOKIE_DOMAIN | - | e.g. .example.com - cross-subdomain cookies for the separate-backend setup |
TRUSTED_PROXY | - | Which proxy may name the client IP: a hop count (1) or a header (cf-connecting-ip). Unset detects known platforms, then the connection peer - see Client IP |
CORS and Better Auth accept exactly the union of config.origins and TRUSTED_ORIGINS - baseUrl is not auto-included. Your production origin must be in one of them or live logins redirect-fail.
Rate limits and cache namespacing
| Var | Effect |
|---|---|
AUTH_RATE_LIMIT_MULTIPLIER | Whole number 1-50 raising every Better Auth rate limit. 1 is shipped; no value disables them - see Rate limits |
API_RATE_LIMIT_MULTIPLIER | The same, for every API budget - global and per-route alike. Raise it for a load test or an automated suite, which drives one address and so shares one bucket |
REDIS_KEY_PREFIX | Prepended to every cache, rate-limit, mutex and session key. Set it when two deployments share one Redis. Empty by default. ioredis cache backend only |
Set REDIS_KEY_PREFIX once, before first boot. Changing it on a running deployment orphans in-flight auto-top-up claims and distributed locks (autotopup:pending:*, withMutex) - drain or use a maintenance window first.
On the Upstash cache backend the variable is refused at boot: the REST SDK has no key-prefix option, and a prefix applied per command would silently miss commands it did not know about. Give each deployment its own Upstash database instead.
NEXT_DIST_DIR moves what next build writes and next start reads, defaulting to .next - it is how the e2e harness builds without disturbing your dev build.
DOCS_NOINDEX (1 or true) makes the docs app ask every crawler to stay away - robots.txt becomes a blanket Disallow: /, the sitemap empties, and every page and rewrite carries noindex, nofollow. Unset by default, which is what you want: your docs are a surface you want found - see Authoring Docs.
How loaders work
Every app and tool reaches the same root .env, by a different mechanism:
| Context | Mechanism |
|---|---|
| App dev scripts | dotenv -e .env -e ../../.env - app-local .env overrides root on conflicts |
| Standalone backend | node --env-file-if-exists ../../.env (dev uses tsx --env-file-if-exists) |
Drizzle / Better Auth CLI (@repo/database) | dotenv -e ../../.env |
| Turbo | .env listed in globalDotEnv so task caches bust on change |
| Tests | Nothing is loaded. pnpm test reads no .env at all - see Testing |
The generated schema
The CLI emits packages/runtime/src/env.ts so feature code typechecks whichever flags you toggle.
- Only the cache choice changes the required shape -
REDIS_URLvs. the Upstash pair. - Everything else (social, payment, email, SMS, storage, captcha, admin alerts, the vendor base URLs) is emitted
optional. - It validates once at module load and throws on a missing or invalid required key.
- Enable a feature by setting its var and flipping the
@repo/configflag, never by editing this file.
Configuration
Control branding, routes, and every flag-gated feature from the one shared @repo/config object, plus the per-app files that own navigation and banners.
Integration keys
Every per-provider environment variable, the feature it powers, the config flag it pairs with, and the base-URL overrides that repoint a vendor.