Skip to content
Reference

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 read

On This Page

Before You Read

Three templates ship in the repo. The keys mean the same thing in each.

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 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() and requireServerEnv(name) from @syntaxkit/shared. They return resolved, validated values (URL, email, and secret-length checks, plus the BETTER_AUTH_URLNEXT_PUBLIC_APP_URL fallback). requireServerEnv throws when a value is missing or invalid. Used in routers, S3/Prisma/PostHog/email/Redis clients, and auth providers.
  • Client seam: publicEnv and isAnalyticsEnabled() from @syntaxkit/shared/client. Each NEXT_PUBLIC_* is captured via a literal process.env reference so Next.js still inlines it. apps/web/lib/env.ts re-exports this as env.
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.

VariableRequiredWhat it controls
DATABASE_URLAlwaysPostgres connection string. Driver adapter is auto-selected (Neon serverless for *.neon.tech, @prisma/adapter-pg elsewhere).
DIRECT_URLOptionalDirect/session-mode URL for prisma migrate deploy when DATABASE_URL points at a transaction-mode pooler. Falls back to DATABASE_URL.
DATABASE_POOL_MAXOptionalCap for the @prisma/adapter-pg pool on non-Neon hosts. No effect on Neon.
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.

Email

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.

VariableRequiredWhat it controls
EMAIL_DELIVERY_MODEOptionallog (dev default), noop (CI), or a real provider mode. Auto-resolves when unset.
EMAIL_FROMReal provider modesSender for every real provider. Bare address or Name <address@domain>. Domain must be verified with the provider.
CONTACT_FORM_TO_EMAILContact form enabledRecipient for public contact-form submissions. Contact also needs Turnstile.
EMAIL_OUTBOX_DIROptionalOverride for the log-mode outbox. Defaults to .local/email-outbox/.
PLUNK_API_KEYMode plunkPlunk transactional API token.
RESEND_API_KEYMode resendResend API key.
POSTMARK_SERVER_TOKENMode postmarkPostmark server token.
BREVO_API_KEYMode brevoBrevo API key.
SENDGRID_API_KEYMode sendgridSendGrid API key.
SMTP_HOSTMode smtpSMTP hostname (required with user and pass).
SMTP_PORTOptionalSMTP port. Defaults to 587.
SMTP_USERMode smtpSMTP username.
SMTP_PASSMode smtpSMTP password.
SMTP_SECUREOptionalOverride 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.

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.
OAUTH_PROXY_SECRETPreview OAuth proxyOpt-in. Same secret (≥32 chars) on production, preview, and local. Routes preview OAuth through the production callback. Distinct from BETTER_AUTH_SECRET.
AUTH_TRUSTED_ORIGINSWith OAuth proxyComma-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)

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. 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.

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 monthly Pro.
STRIPE_PRICE_ID_PRO_YEARLYBilling enabledPrice id for yearly Pro.
STRIPE_PRICE_ID_ULTRA_MONTHLYBilling enabledPrice id for monthly Ultra.
STRIPE_PRICE_ID_ULTRA_YEARLYBilling enabledPrice id for yearly Ultra.
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.
BILLING_REQUIREDOptionalDoctor 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.

VariableRequiredWhat it controls
NEXT_PUBLIC_POSTHOG_KEYAnalytics enabledPostHog project API key. The only gate.
NEXT_PUBLIC_POSTHOG_HOSTOptionalIngest host. Defaults to https://us.i.posthog.com.
NEXT_PUBLIC_POSTHOG_UI_HOSTOptionalUI host for session-replay deep links. Defaults to https://us.posthog.com.
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)

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.

VariableRequiredWhat it controls
NEXT_PUBLIC_S3_PUBLIC_URLStorage enabledPublic base URL used to compose served image URLs.
NEXT_PUBLIC_S3_BUCKET_NAME_IMAGESStorage enabledPublic bucket the kit reads and writes for avatars and logos.
S3_BUCKET_NAME_ATTACHMENTSChat attachmentsServer-only private bucket name. Requires base storage. Never point this at the public images bucket.
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.
AWS_ENDPOINT_URL_S3Non-AWS providersCustom endpoint for Cloudflare R2, MinIO, etc. Leave blank for native AWS S3.
AWS_REGIONOptionalRegion passed to the SDK. Defaults to "auto". Set an AWS region for native 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 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.

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 does not 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. A production server always runs full validation and serves a configuration-error page (with /api/health 503) on misconfiguration.

Where To Go Next

Also useful: Security, Going To Production, and Commands And Scripts.

Was this page helpful?

On this page