Skip to content

Environment Variables

This page is the single source of truth for every environment variable the app reads. Start from .env.example:

bash
cp .env.example .env

Most variables are validated at startup by a Zod schema in src/config/env.ts - if something required is missing or malformed, the app fails fast at boot with a readable error instead of failing later at the call site. A few variables (marked below) are read directly from process.env where they're used and aren't part of that schema.

SKIP_ENV_VALIDATION=1 bypasses the Zod validation entirely. It's set automatically during the Docker build stage (see deployment.md) so a missing production .env file doesn't fail the build - real validation still runs when the container actually starts.

Database

VariableRequiredDefaultDescription
DATABASE_URLYes-PostgreSQL connection string. The value in .env.example matches docker-compose.yml's Postgres service out of the box.

Better Auth

VariableRequiredDefaultDescription
BETTER_AUTH_SECRETYes-Session/cookie encryption key. Generate with openssl rand -base64 32.
BETTER_AUTH_URLYes-The app's own base URL (e.g. http://localhost:3000, or your production domain). Used to build OAuth callback URLs and auth cookies.

OAuth Providers (optional)

VariableRequiredDefaultDescription
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRETNo-From Google Cloud Console. Leave both unset to disable Google sign-in.
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRETNo-From GitHub Developer Settings. Leave both unset to disable GitHub sign-in.

Both providers are wired conditionally in src/lib/auth.ts - a provider block is only added to Better Auth's socialProviders config if its id/secret pair is present. See auth-and-billing.md for setup steps.

Inngest (background jobs)

VariableRequiredDefaultDescription
INNGEST_APP_IDYes-Any unique identifier for your app, used by the Inngest client (src/inngest/client.ts).
NEXT_PUBLIC_INNGEST_BASE_URLYes-Browser-facing. The URL the Inngest Realtime WebSocket connects to from the client (used by /demo/inngest's live status updates). Must be reachable from the user's browser - in a Docker deployment this can not be the Docker-internal http://inngest:8288 hostname; expose the Inngest server through your reverse proxy and point this at that public URL instead.
INNGEST_BASE_URLProduction only-Server-side. Not in .env.example - set directly in docker-compose.prod.yml, overridden to the Docker-internal http://inngest:8288 hostname so the Next.js server can reach the Inngest container. Only relevant for Docker deployments.
INNGEST_SIGNING_KEY / INNGEST_EVENT_KEYNo-Not in .env.example. Only needed if you switch the inngest container from inngest dev to inngest start for durable job storage - see the Inngest section of deployment.md for why src/inngest/client.ts hardcodes isDev: true and what that means for these keys.

AI (optional)

VariableRequiredDefaultDescription
OPENROUTER_API_KEYNo-From openrouter.ai. Required only for the /demo/ai chat demo and any code using src/lib/ai.ts. The demo page shows a warning card instead of crashing when this is unset.
AI_MODELNogoogle/gemini-2.5-flashAny OpenRouter model id. Read by src/lib/ai.ts.

Redis

VariableRequiredDefaultDescription
REDIS_URLNoredis://localhost:6379Redis connection string. The default matches docker-compose.yml's Redis service.

Polar (payments, optional)

VariableRequiredDefaultDescription
POLAR_ACCESS_TOKENNo-From your Polar dashboard.
POLAR_SUCCESS_URLNo-Redirect URL after a successful checkout (e.g. http://localhost:3000/dashboard).
POLAR_SERVERNo-sandbox for testing, production for live.

All three must be set together - src/lib/polar.ts derives isPolarEnabled = !!(POLAR_ACCESS_TOKEN && POLAR_SERVER && POLAR_SUCCESS_URL). Leave all three unset to disable billing entirely: the Polar plugin is never registered with Better Auth, and premiumProcedure / useHasActiveSubscription() both grant everyone premium access as a local dev/mock mode. See auth-and-billing.md.

Resend (email, optional)

VariableRequiredDefaultDescription
RESEND_API_KEYNo-From your Resend dashboard.
EMAIL_FROMNo-The from address/name used for outgoing emails (e.g. Acme <noreply@example.com>). Must be a verified domain in Resend for production sending.

Both must be set together - src/lib/resend.ts derives isResendEnabled = !!(RESEND_API_KEY && EMAIL_FROM). Leave both unset to disable real sending: src/lib/mailer.ts's sendEmail() logs the email (recipient, subject, and body/link) to the console instead, so password-reset and email-verification links can still be copied out of the terminal during local development. See auth-and-billing.md.

Analytics (optional)

VariableRequiredDefaultDescription
NEXT_PUBLIC_PLAUSIBLE_SRCNo-Read directly via process.env.NEXT_PUBLIC_PLAUSIBLE_SRC in src/app/layout.tsx (not part of the Zod schema, since it's optional and public). Your Plausible site script URL (self-hosted or plausible.io). Leave unset to disable analytics - no script is rendered and <PlausibleProvider> becomes a no-op. See integrations.md.

Notes

  • NEXT_PUBLIC_* variables are inlined into the client bundle at build time (standard Next.js behavior) - never put secrets in one.
  • After changing any NEXT_PUBLIC_* variable, restart the dev server (and rebuild for Docker) - they're baked in at build time, not read at runtime in the browser.
  • See getting-started.md for the full local setup flow, and deployment.md for production-specific overrides.