AUTH STARTER · ENVIRONMENT VARIABLES
Environment variables
Every env var, why it exists, and what happens if it's missing. Validated with T3 Env + Zod — the app refuses to boot with a typo.
We don't read process.env directly anywhere except src/lib/env.ts. Every variable is declared there, validated by Zod on startup, and exported as a typed object. Typos are caught at boot, not at 2am in production.
Required (the app won't boot without these)
# Database
DATABASE_URL="postgresql://user:pass@host/db?sslmode=require"
# Better Auth
BETTER_AUTH_SECRET="..." # 32+ chars, openssl rand -base64 32
BETTER_AUTH_URL="http://localhost:3000"
NEXT_PUBLIC_APP_URL="http://localhost:3000"Optional (features turn on when you fill them in)
# Unpooled DB connection — used only by migrations
DATABASE_URL_UNPOOLED="postgresql://..."
# OAuth — Google
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
# OAuth — GitHub
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
# Email (Resend) — required for magic links, OTP, password reset
RESEND_API_KEY=""
EMAIL_FROM="You <hello@yourdomain.com>"
# Captcha (Cloudflare Turnstile)
TURNSTILE_SECRET_KEY=""
NEXT_PUBLIC_TURNSTILE_SITE_KEY=""Adding a new variable
Three steps, in this order:
- Add it to src/lib/env.ts (server or client section + runtimeEnv mapping).
- Add a placeholder to .env.example.
- If secret → reference via env in auth.config.server.ts. If public → use process.env.NEXT_PUBLIC_* in auth.config.ts.
Why emptyStringAsUndefined matters
Hosting platforms (Vercel especially) often set undefined env vars as empty strings instead of leaving them undefined. We set emptyStringAsUndefined: true so optional vars don't fail Zod validation. If you add a NEW required string field, an empty value will be treated as missing — which is what you want.
process.env.X is forbidden everywhere except env.ts and the configs
Reading process.env.X directly bypasses validation. A typo silently passes. Always import { env } from "@/lib/env" so TypeScript and Zod have your back.