GenerateSaaS

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.

VarPurpose
DATABASE_URLPostgres connection string
REDIS_URLCache + rate-limit store. Upstash builds require UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN instead
BETTER_AUTH_SECRETMin 32 chars, powers @repo/auth - openssl rand -hex 32
API_URLBackend 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_URLBackground 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

VarBacksNotes
BASE_URLconfig.baseUrlThe public origin. Server code prefers it over the committed literal for auth redirects, email links, device verification and checkout returns
TRUSTED_ORIGINSconfig.originsComma-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

VarEffect
AUTH_RATE_LIMIT_MULTIPLIERWhole number 1-50 raising every Better Auth rate limit. 1 is shipped; no value disables them - see Rate limits
API_RATE_LIMIT_MULTIPLIERThe 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_PREFIXPrepended 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:

ContextMechanism
App dev scriptsdotenv -e .env -e ../../.env - app-local .env overrides root on conflicts
Standalone backendnode --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
TestsNothing 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_URL vs. 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/config flag, never by editing this file.

On this page