Environment Variables
Every variable the kit reads, where it is used, and what it controls. A fresh clone runs with three core vars (DATABASE_URL, NEXT_PUBLIC_APP_URL, BETTER_AUTH_SECRET). Optional groups degrade cleanly when blank.
Last updated on
10 min readOn This Page
Before You Read
Three templates ship in the repo. The keys mean the same thing in each.
| Template | Copy to | What it's for |
|---|---|---|
apps/web/.env.example | apps/web/.env | Local dev and production. The canonical surface. |
apps/web/.env.test.example | apps/web/.env.test | CI and Playwright. Includes the test escapes at the bottom of this page. |
.env.docker.example | .env (repo root) | Self-hosting via docker-compose.yml. Adds NEXT_SERVER_ACTIONS_ENCRYPTION_KEY for multi-instance deploys. |
packages/database/.env.example is a Prisma CLI shim that points at apps/web/.env. Leave it alone.
Build-Time vs Runtime
Every NEXT_PUBLIC_* var is inlined into the JS bundle at build time. Changing one needs a rebuild on every host. Everything else is read at runtime. See Deployment: build-time vs runtime env for the per-host guide.
How The Kit Reads Env
App code does not read process.env.<NAME> for these variables directly. It goes through two seams. Both are backed by the validated catalog in packages/shared/src/setup.
- Server seam:
getServerEnv()andrequireServerEnv(name)from@syntaxkit/shared. They return resolved, validated values (URL, email, and secret-length checks, plus theBETTER_AUTH_URL→NEXT_PUBLIC_APP_URLfallback).requireServerEnvthrows when a value is missing or invalid. Used in routers, S3/Prisma/PostHog/email/Redis clients, and auth providers. - Client seam:
publicEnvandisAnalyticsEnabled()from@syntaxkit/shared/client. EachNEXT_PUBLIC_*is captured via a literalprocess.envreference so Next.js still inlines it.apps/web/lib/env.tsre-exports this asenv.
What keeps the seam from being bypassed?
Two guards. apps/web/lib/env.server.ts runs the shared setup catalog (getSetupState) and, on fatal errors, serves a configuration-error page with /api/health 503. An ESLint rule (no-restricted-syntax in @syntaxkit/eslint-config) blocks new direct process.env reads of the validated server vars. Operational vars (NODE_ENV, NEXT_RUNTIME, …) and NEXT_PUBLIC_* literals stay allowed.
Required For Any Deploy
The minimum surface every deploy needs.
| Variable | Required | What it controls |
|---|---|---|
DATABASE_URL | Always | Postgres connection string. Driver adapter is auto-selected (Neon serverless for *.neon.tech, @prisma/adapter-pg elsewhere). |
DIRECT_URL | Optional | Direct/session-mode URL for prisma migrate deploy when DATABASE_URL points at a transaction-mode pooler. Falls back to DATABASE_URL. |
DATABASE_POOL_MAX | Optional | Cap for the @prisma/adapter-pg pool on non-Neon hosts. No effect on Neon. |
NEXT_PUBLIC_APP_URL | Always | Canonical app origin. Drives metadata, OG image, CSP, CORS, OAuth callbacks, and sitemap entries. |
BETTER_AUTH_SECRET | Always | Session cookie encryption. Generate with openssl rand -base64 32. Minimum 32 characters. |
See Database and Authentication.
Production must use a real provider (plunk, resend, postmark, brevo, sendgrid, or smtp) with that provider's credentials and EMAIL_FROM. log and noop are rejected at boot in production.
When EMAIL_DELIVERY_MODE is unset, the kit picks the single configured provider credential (Plunk first in precedence). With no provider it uses noop in tests and log otherwise. If more than one provider key is set, set EMAIL_DELIVERY_MODE explicitly or boot fails.
| Variable | Required | What it controls |
|---|---|---|
EMAIL_DELIVERY_MODE | Optional | log (dev default), noop (CI), or a real provider mode. Auto-resolves when unset. |
EMAIL_FROM | Real provider modes | Sender for every real provider. Bare address or Name <address@domain>. Domain must be verified with the provider. |
CONTACT_FORM_TO_EMAIL | Contact form enabled | Recipient for public contact-form submissions. Contact also needs Turnstile. |
EMAIL_OUTBOX_DIR | Optional | Override for the log-mode outbox. Defaults to .local/email-outbox/. |
PLUNK_API_KEY | Mode plunk | Plunk transactional API token. |
RESEND_API_KEY | Mode resend | Resend API key. |
POSTMARK_SERVER_TOKEN | Mode postmark | Postmark server token. |
BREVO_API_KEY | Mode brevo | Brevo API key. |
SENDGRID_API_KEY | Mode sendgrid | SendGrid API key. |
SMTP_HOST | Mode smtp | SMTP hostname (required with user and pass). |
SMTP_PORT | Optional | SMTP port. Defaults to 587. |
SMTP_USER | Mode smtp | SMTP username. |
SMTP_PASS | Mode smtp | SMTP password. |
SMTP_SECURE | Optional | Override TLS behavior. Defaults to secure on port 465, STARTTLS otherwise. |
See Email for delivery modes, templates, and provider swaps.
OAuth
Both halves of a provider pair must be set together. Missing either disables that provider: the button still renders, but disabled.
| Variable | Required | What it controls |
|---|---|---|
GITHUB_CLIENT_ID | GitHub OAuth enabled | Client id from github.com/settings/developers. |
GITHUB_CLIENT_SECRET | GitHub OAuth enabled | Matching app secret. |
GOOGLE_CLIENT_ID | Google OAuth enabled | Client id from console.cloud.google.com/apis/credentials. |
GOOGLE_CLIENT_SECRET | Google OAuth enabled | Matching client secret. |
OAUTH_PROXY_SECRET | Preview OAuth proxy | Opt-in. Same secret (≥32 chars) on production, preview, and local. Routes preview OAuth through the production callback. Distinct from BETTER_AUTH_SECRET. |
AUTH_TRUSTED_ORIGINS | With OAuth proxy | Comma-separated extra trusted redirect origins (wildcards allowed, e.g. https://*.vercel.app). |
Callback URLs must point at <NEXT_PUBLIC_APP_URL>/api/auth/callback/<provider>. With the proxy, register only the production callback and pin NEXT_PUBLIC_APP_URL / BETTER_AUTH_URL to the production origin in every environment. See Authentication.
Captcha (Cloudflare Turnstile)
| Variable | Required | What it controls |
|---|---|---|
TURNSTILE_SECRET_KEY | Captcha enabled | Server-side secret used by Better Auth's captcha plugin and verifyTurnstileToken in the contact form. |
NEXT_PUBLIC_TURNSTILE_SITE_KEY | Captcha enabled | Public site key for the browser widget. |
Both must be set. Either missing disables the captcha plugin and the contact-form check. See Security.
Stripe Billing
Billing enables when STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET are both set. Price id names come from the billing catalog in @syntaxkit/payments. A partial set (secrets without all declared price ids, or the reverse) fails at boot.
| Variable | Required | What it controls |
|---|---|---|
STRIPE_SECRET_KEY | Billing enabled | Server-side Stripe API key (sk_live_* in production, sk_test_* elsewhere). |
STRIPE_WEBHOOK_SECRET | Billing enabled | Verifies webhook signatures. The production secret differs from the dev / Stripe CLI one. |
STRIPE_PRICE_ID_PRO_MONTHLY | Billing enabled | Price id for monthly Pro. |
STRIPE_PRICE_ID_PRO_YEARLY | Billing enabled | Price id for yearly Pro. |
STRIPE_PRICE_ID_ULTRA_MONTHLY | Billing enabled | Price id for monthly Ultra. |
STRIPE_PRICE_ID_ULTRA_YEARLY | Billing enabled | Price id for yearly Ultra. |
STRIPE_AUTOMATIC_TAX | Optional | Set to "true" to enable Stripe Tax on Checkout (automatic_tax, tax_id_collection, customer_update, required billing address). Needs Stripe Tax activated with a registration in the Dashboard, or Checkout fails. Leave unset otherwise. |
BILLING_REQUIRED | Optional | Doctor only. Set to "true" so pnpm setup:doctor fails when billing is missing or partial. The app ignores this flag. |
See Billing for the plan catalog and Webhooks And Async Workflows for the inbound contract.
PostHog Analytics And Monitoring
Analytics gates on NEXT_PUBLIC_POSTHOG_KEY alone. Host vars are optional endpoint overrides. They default to US Cloud in code. Leave them blank unless you use EU Cloud or self-host. Missing the key disables analytics, error shipping, and related helpers with no warnings and no broken UI.
| Variable | Required | What it controls |
|---|---|---|
NEXT_PUBLIC_POSTHOG_KEY | Analytics enabled | PostHog project API key. The only gate. |
NEXT_PUBLIC_POSTHOG_HOST | Optional | Ingest host. Defaults to https://us.i.posthog.com. |
NEXT_PUBLIC_POSTHOG_UI_HOST | Optional | UI host for session-replay deep links. Defaults to https://us.posthog.com. |
POSTHOG_PROXY_INGEST_HOST | Optional | Reverse-proxy target for /ingest/*. Set both proxy hosts to route browser traffic through your origin. |
POSTHOG_PROXY_ASSET_HOST | Optional | Reverse-proxy target for /ingest/static/*. |
POSTHOG_API_KEY | Source-map upload | Personal API key with project scope. Read at build time by withPostHogConfig. |
POSTHOG_PROJECT_ID | Source-map upload | PostHog project id. Build-time only. |
See Analytics and Monitoring.
Object Storage (S3)
Public storage (avatars and org logos) enables when the public URL, images bucket, and AWS credentials are all set. Chat attachments need a separate private bucket on top of that. Partial public-storage config disables uploads and warns; it does not crash the app.
| Variable | Required | What it controls |
|---|---|---|
NEXT_PUBLIC_S3_PUBLIC_URL | Storage enabled | Public base URL used to compose served image URLs. |
NEXT_PUBLIC_S3_BUCKET_NAME_IMAGES | Storage enabled | Public bucket the kit reads and writes for avatars and logos. |
S3_BUCKET_NAME_ATTACHMENTS | Chat attachments | Server-only private bucket name. Requires base storage. Never point this at the public images bucket. |
NEXT_PUBLIC_IMAGE_HOST_ALLOWLIST | Optional | Comma-separated extra hostnames accepted on user-supplied image URLs (org logos, avatars). Hosts from NEXT_PUBLIC_S3_PUBLIC_URL / AWS_ENDPOINT_URL_S3 plus built-in defaults (DiceBear, GitHub/Google/Gravatar) are always allowed. |
AWS_ENDPOINT_URL_S3 | Non-AWS providers | Custom endpoint for Cloudflare R2, MinIO, etc. Leave blank for native AWS S3. |
AWS_REGION | Optional | Region passed to the SDK. Defaults to "auto". Set an AWS region for native S3. |
AWS_ACCESS_KEY_ID | Storage enabled | Credential for signing presigned URLs and finalize PUTs. |
AWS_SECRET_ACCESS_KEY | Storage enabled | Paired secret for the access key. |
AWS_S3_FORCE_PATH_STYLE | MinIO and similar | Set to "true" for path-style endpoints. AWS S3 and R2 use the default (false). |
See Storage for the upload pipeline, bucket config, and provider tabs.
Abuse Protection (Upstash Redis)
| Variable | Required | What it controls |
|---|---|---|
UPSTASH_REDIS_REST_URL | Yes in production | Upstash Redis REST URL. Both vars must be set together. |
UPSTASH_REDIS_REST_TOKEN | Yes in production | Upstash Redis REST token. |
Production will not serve normally without both; the setup catalog treats that as a fatal config error. Non-production auto-bypasses missing values (one-time dev_no_upstash warning) so pnpm dev works.
What happens when abuse protection is unavailable in production?
Every protected surface (uploads, chat, auth emails, and the contact form) fails closed with SERVICE_UNAVAILABLE. See Security for the full posture matrix and the DISABLE_ABUSE_PROTECTION non-production escape hatch.
Operational
Things that don't fit a single subsystem.
| Variable | Required | What it controls |
|---|---|---|
BETTER_AUTH_URL | Optional | Override the auto-derived auth origin. Usually unset. Better Auth derives it from NEXT_PUBLIC_APP_URL. |
NEXT_PUBLIC_DOCS_URL | Optional | Public docs URL for marketing footer and dashboard help links. |
TRUST_PROXY_HEADERS | Behind a load balancer | Set to "true" so Better Auth reads the real client IP from forwarded headers. Render sets this by default; Fly's edge-only setup does not need it. |
TRUSTED_PROXY_IP_HEADERS | Behind a load balancer | Comma-separated header names to trust (e.g. cf-connecting-ip, x-forwarded-for). |
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY | Multi-instance deploys | 32+ random bytes (openssl rand -base64 32) shared across replicas so server-action signatures decrypt across hosts. |
See Security: operational secrets and Deployment for per-host placement.
Test And CI Escapes
Never set these in production. They exist for deterministic test runs only. The Security pre-launch checklist verifies each one before launch.
| Variable | Set in | What it controls |
|---|---|---|
DISABLE_ABUSE_PROTECTION | apps/web/.env.test, Playwright | Bypasses the Upstash abuse policy across every protected surface. Hard-blocked at boot in production. |
DISABLE_CAPTCHA_FOR_TESTS / NEXT_PUBLIC_DISABLE_CAPTCHA_FOR_TESTS | apps/web/.env.test, Playwright | Bypasses server-side Turnstile verification and the client widget so CI never waits on test-key roundtrips. Hard-blocked at boot in production. |
RUN_STRIPE_LIVE | Stripe live test suite | Set to "1" to opt in to pnpm test:stripe against the real Stripe test-mode API. Off ("0") by default. |
Better Auth's per-route rate limits also widen for tests, but with no env var: the widening is gated on NODE_ENV === "test" directly in packages/auth/src/server.ts, so production can never disable them.
There is no env var to bypass server-side validation at runtime. next build is detected via PHASE_PRODUCTION_BUILD and tests via NODE_ENV === "test", so build pipelines and test runs work without a flag. A production server always runs full validation and serves a configuration-error page (with /api/health 503) on misconfiguration.
Where To Go Next
Setup
The env-driven first-run walkthrough that validates this whole list with pnpm setup:doctor.
Deployment
Build-time vs runtime split per host, plus per-platform setup for the operational vars.
Also useful: Security, Going To Production, and Commands And Scripts.
Commands And Scripts
pnpm and Turborepo tasks for the monorepo. Most run from the repo root; a few live only in the package that owns them.
Package Map
Every workspace package: what it owns, what it exports, and where the deep dive lives. Use Project Structure for the visual repo tour; follow each row's deep-dive link for a subsystem narrative.
