Skip to content
Docs just relaunched - explore the new sidebar, OG images, and AI-ready content.
Reference

Environment Variables

Every variable, where it is used, and what it controls.

Last updated on

8 min read

Every variable the kit reads, grouped by purpose. A fresh clone runs with just three set (DATABASE_URL, NEXT_PUBLIC_APP_URL, and BETTER_AUTH_SECRET), and every optional group degrades cleanly when left blank (auth-only, billing-off, no-storage, no-analytics).

Before You Read

Three templates ship in the repo. Whichever you start from, the keys mean the same thing.

TemplateCopy toWhat it's for
apps/web/.env.exampleapps/web/.envLocal dev and production. The canonical surface.
apps/web/.env.test.exampleapps/web/.env.testCI and Playwright. Includes the test escapes documented at the bottom.
.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, so 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 never reads process.env.<NAME> for these variables directly. It goes through two seams, both backed by the validated catalog in packages/shared/src/setup (the single source of truth):

  • Server seam: getServerEnv() and requireServerEnv(name) from @syntaxkit/shared. They return resolved, validated values (URL/email/secret-length checks, the BETTER_AUTH_URLNEXT_PUBLIC_APP_URL fallback). requireServerEnv throws when a value is missing or invalid. Used in server code (routers, S3/Prisma/PostHog/Plunk/Redis clients, auth providers).
  • Client seam: publicEnv and isAnalyticsEnabled() from @syntaxkit/shared/client. Captures each NEXT_PUBLIC_* via a literal process.env reference so Next.js still inlines it, then derives client capability flags. apps/web/lib/env.ts re-exports it as env.
What keeps the seam from being bypassed?

Two guards. A boot-time hard stop (assertValidSetupEnv, wired through apps/web/lib/env.server.ts) refuses to start a misconfigured production deploy. And 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.

VariableRequiredWhat it controls
DATABASE_URLAlwaysPostgres connection string. Driver adapter is auto-selected (Neon serverless for *.neon.tech, @prisma/adapter-pg elsewhere).
NEXT_PUBLIC_APP_URLAlwaysCanonical app origin. Drives metadata, OG image, CSP, CORS, OAuth callbacks, and sitemap entries.
BETTER_AUTH_SECRETAlwaysSession cookie encryption. Generate with openssl rand -base64 32; minimum 32 characters.

See Database and Authentication for deeper context.

Email

VariableRequiredWhat it controls
EMAIL_DELIVERY_MODEOptionallog (dev default), noop (CI), or plunk (production). Auto-resolves to plunk when PLUNK_API_KEY is set, noop when NODE_ENV=test, else log. Production rejects noop and log at boot: both report success without delivering, which would silently drop signups, resets, and invitations.
PLUNK_API_KEYWhen mode is plunkToken for Plunk's transactional API.
CONTACT_FORM_TO_EMAILContact form enabledRecipient for public contact-form submissions.
EMAIL_OUTBOX_DIROptionalOverride for the log-mode outbox. Defaults to .local/email-outbox/.

See Email for delivery modes, templates, and the swap-providers walkthrough.

OAuth

Both halves of a provider pair must be set together. Missing either disables that provider gracefully: the button still renders, but disabled.

VariableRequiredWhat it controls
GITHUB_CLIENT_IDGitHub OAuth enabledClient id from github.com/settings/developers.
GITHUB_CLIENT_SECRETGitHub OAuth enabledMatching app secret.
GOOGLE_CLIENT_IDGoogle OAuth enabledClient id from console.cloud.google.com/apis/credentials.
GOOGLE_CLIENT_SECRETGoogle OAuth enabledMatching client secret.

Callback URLs must point at <NEXT_PUBLIC_APP_URL>/api/auth/callback/<provider>. See Authentication.

Captcha (Cloudflare Turnstile)

VariableRequiredWhat it controls
TURNSTILE_SECRET_KEYCaptcha enabledServer-side secret used by Better Auth's captcha plugin and verifyTurnstileToken in the contact form.
NEXT_PUBLIC_TURNSTILE_SITE_KEYCaptcha enabledPublic site key for the browser widget.

Both must be set; either missing disables the captcha plugin and the contact-form check gracefully. See Security.

Stripe Billing

VariableRequiredWhat it controls
STRIPE_SECRET_KEYBilling enabledServer-side Stripe API key (sk_live_* in production, sk_test_* elsewhere).
STRIPE_WEBHOOK_SECRETBilling enabledVerifies webhook signatures. The production secret differs from the dev / Stripe-CLI one.
STRIPE_PRICE_ID_PRO_MONTHLYBilling enabledPrice id for the monthly Pro plan. The kit fails at startup if either price id is missing.
STRIPE_PRICE_ID_PRO_YEARLYBilling enabledPrice id for the yearly Pro plan.
STRIPE_AUTOMATIC_TAXOptionalSet 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.

See Billing for the plan catalog and Webhooks And Async Workflows for the inbound contract.

PostHog Analytics And Monitoring

The first three activate analytics, error tracking, and structured logs. Missing any one disables the integration cleanly. The rest enable optional features (reverse proxy, source-map upload).

VariableRequiredWhat it controls
NEXT_PUBLIC_POSTHOG_KEYAnalytics enabledPostHog project API key.
NEXT_PUBLIC_POSTHOG_HOSTAnalytics enabledIngest host (e.g. https://us.i.posthog.com).
NEXT_PUBLIC_POSTHOG_UI_HOSTAnalytics enabledUI host for session-replay deep links.
POSTHOG_PROXY_INGEST_HOSTOptionalReverse-proxy target for /ingest/*. Set both proxy hosts to route browser traffic through your origin.
POSTHOG_PROXY_ASSET_HOSTOptionalReverse-proxy target for /ingest/static/*.
POSTHOG_API_KEYSource-map uploadPersonal API key with project scope. Read at build time by withPostHogConfig.
POSTHOG_PROJECT_IDSource-map uploadPostHog project id. Build-time only.

See Analytics and Monitoring.

Object Storage (S3)

VariableRequiredWhat it controls
NEXT_PUBLIC_S3_PUBLIC_URLStorage enabledPublic base URL used to compose served image URLs.
NEXT_PUBLIC_S3_BUCKET_NAME_IMAGESStorage enabledBucket the kit reads and writes.
NEXT_PUBLIC_IMAGE_HOST_ALLOWLISTOptionalComma-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; this is the escape hatch for extra CDN domains.
AWS_ENDPOINT_URL_S3Non-AWS providersCustom endpoint for Cloudflare R2, MinIO, etc. Leave blank for native AWS S3.
AWS_REGIONStorage enabledRegion passed to the SDK. Defaults to "auto"; required by AWS S3.
AWS_ACCESS_KEY_IDStorage enabledCredential for signing presigned URLs and finalize PUTs.
AWS_SECRET_ACCESS_KEYStorage enabledPaired secret for the access key.
AWS_S3_FORCE_PATH_STYLEMinIO and similarSet 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)

VariableRequiredWhat it controls
UPSTASH_REDIS_REST_URLYes in productionUpstash Redis REST URL. Both vars must be set together.
UPSTASH_REDIS_REST_TOKENYes in productionUpstash Redis REST token.

Production won't boot without both; assertValidSetupEnv errors out otherwise. Non-production auto-bypasses missing values (one-time dev_no_upstash warning) so pnpm dev just 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.

VariableRequiredWhat it controls
BETTER_AUTH_URLOptionalOverride the auto-derived auth origin. Usually unset. Better Auth derives it from NEXT_PUBLIC_APP_URL.
NEXT_PUBLIC_DOCS_URLOptionalPublic docs URL for marketing footer and dashboard help links.
TRUST_PROXY_HEADERSBehind a load balancerSet to "true" so Better Auth reads the real client IP from forwarded headers. Render sets this by default; Fly's edge-only setup doesn't need it.
TRUSTED_PROXY_IP_HEADERSBehind a load balancerComma-separated header names to trust (e.g. cf-connecting-ip, x-forwarded-for).
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYMulti-instance deploys32+ 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.

VariableSet inWhat it controls
DISABLE_ABUSE_PROTECTIONapps/web/.env.test, PlaywrightBypasses the Upstash abuse policy across every protected surface. Hard-blocked at boot in production.
DISABLE_CAPTCHA_FOR_TESTS / NEXT_PUBLIC_DISABLE_CAPTCHA_FOR_TESTSapps/web/.env.test, PlaywrightBypasses server-side Turnstile verification and the client widget so CI never waits on test-key roundtrips. Hard-blocked at boot in production.
RUN_STRIPE_LIVEStripe live test suiteSet 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, but a production server always boots through full validation and refuses to start on misconfiguration.

Where To Go Next

Was this page helpful?

On this page