Appearance
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 .envMost 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
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL | Yes | - | PostgreSQL connection string. The value in .env.example matches docker-compose.yml's Postgres service out of the box. |
Better Auth
| Variable | Required | Default | Description |
|---|---|---|---|
BETTER_AUTH_SECRET | Yes | - | Session/cookie encryption key. Generate with openssl rand -base64 32. |
BETTER_AUTH_URL | Yes | - | 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)
| Variable | Required | Default | Description |
|---|---|---|---|
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | No | - | From Google Cloud Console. Leave both unset to disable Google sign-in. |
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET | No | - | 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)
| Variable | Required | Default | Description |
|---|---|---|---|
INNGEST_APP_ID | Yes | - | Any unique identifier for your app, used by the Inngest client (src/inngest/client.ts). |
NEXT_PUBLIC_INNGEST_BASE_URL | Yes | - | 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_URL | Production 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_KEY | No | - | 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)
| Variable | Required | Default | Description |
|---|---|---|---|
OPENROUTER_API_KEY | No | - | 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_MODEL | No | google/gemini-2.5-flash | Any OpenRouter model id. Read by src/lib/ai.ts. |
Redis
| Variable | Required | Default | Description |
|---|---|---|---|
REDIS_URL | No | redis://localhost:6379 | Redis connection string. The default matches docker-compose.yml's Redis service. |
Polar (payments, optional)
| Variable | Required | Default | Description |
|---|---|---|---|
POLAR_ACCESS_TOKEN | No | - | From your Polar dashboard. |
POLAR_SUCCESS_URL | No | - | Redirect URL after a successful checkout (e.g. http://localhost:3000/dashboard). |
POLAR_SERVER | No | - | 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)
| Variable | Required | Default | Description |
|---|---|---|---|
RESEND_API_KEY | No | - | From your Resend dashboard. |
EMAIL_FROM | No | - | 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)
| Variable | Required | Default | Description |
|---|---|---|---|
NEXT_PUBLIC_PLAUSIBLE_SRC | No | - | 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.mdfor the full local setup flow, anddeployment.mdfor production-specific overrides.