Configuration
Learn which environment variables env.ts requires, which ones are optional, and what each optional group configures.
env.ts is the environment contract: a createEnv schema from @t3-oss/env-nextjs that validates every variable at startup and splits them into server-only and NEXT_PUBLIC_* client values.
.env.example is the starting point for a local file. It carries every key env.ts declares except VERCEL_URL, which Vercel injects, and most of them are empty strings.
Environment variables are credentials and switches only. What an operator tunes day to day lives under app/admin/settings/ and changes without a deploy.
What "missing" means
Two behaviours of the schema decide whether a variable counts as set.
emptyStringAsUndefined is on, so KEY="" is the same as not setting KEY at all. The empty values in .env.example do not satisfy a required variable.
Validation runs at module load, so one absent required value fails every request with Invalid environment variables and names the exact keys. It is not a per-route failure and it does not wait for the code path that reads the variable.
Setting SKIP_ENV_VALIDATION skips the check entirely. The email script sets it, and vitest.config.ts sets it for the unit suite.
Required
Every variable in this section is non-optional in env.ts. The app does not boot without them.
NEXT_PUBLIC_SITE_URL
NEXT_PUBLIC_SITE_EMAIL
DATABASE_URL
BETTER_AUTH_URL
BETTER_AUTH_SECRET
REDIS_REST_URL
REDIS_REST_TOKEN
RESEND_API_KEY
RESEND_SENDER_EMAIL
S3_REGION
S3_BUCKET
S3_ACCESS_KEY
S3_SECRET_ACCESS_KEYNEXT_PUBLIC_SITE_URL is the public URL, validated as a URL. Emails, feeds, JSON-LD and checkout links embed it, so it is not only used for links inside the browser.
NEXT_PUBLIC_SITE_EMAIL is the public contact address, validated as an email.
DATABASE_URL is the Postgres connection string, validated as a URL.
BETTER_AUTH_URL is the auth base URL — the site URL in most setups. BETTER_AUTH_SECRET signs sessions.
REDIS_REST_URL and REDIS_REST_TOKEN are the Upstash Redis REST endpoint and its token.
RESEND_API_KEY matters more than its position in the list suggests: sign-in is a magic link, so without this nobody can log in — including into the admin. RESEND_SENDER_EMAIL is the from-address for transactional email, validated as an email.
The four S3_* values configure media storage. Any S3-compatible provider works.
Defaults
Five variables carry defaults and can be left unset:
PORT(8000)NODE_ENV(development)VERCEL_ENV(development)AI_PROVIDER(google)ENABLE_TAXES(false)
VERCEL_URL is optional and transformed — env.ts prefixes whatever Vercel injects with https://.
The DB_USER, DB_PASSWORD and DB_NAME entries in .env.example are read by compose.yml, not by env.ts.
Local development
checkRateLimit in lib/rate-limit/presets.ts returns success immediately
when isDev, so the limiter never calls Redis locally and the two
REDIS_REST_* values only need to be set. The Self-host readiness panel does
call redis.ping(), so with placeholder values it will report Redis down.
Payments — Stripe
Two variables turn payment on.
STRIPE_SECRET_KEY is what lets checkout take payment at all.
STRIPE_WEBHOOK_SECRET is what app/api/stripe/webhooks/route.ts uses to verify incoming events.
The store browses and carts without Stripe configured. Checkout is what switches off.
Tax is a separate gate, and it is worth keeping the two apart. ENABLE_TAXES (default false) decides whether any tax is charged at all.
Each channel's stored taxProvider then selects between two behaviours:
manual— the flat channel ratestripe_tax— real jurisdiction tax, which falls back to the flat rate on any Stripe error
A stored provider name that is not one of those two logs a warning and charges the flat rate.
AI
AI_PROVIDER selects google, anthropic or openai. It defaults to google.
The key you supply has to match that choice: GOOGLE_GENERATIVE_AI_API_KEY, ANTHROPIC_API_KEY or OPENAI_API_KEY. lib/ai/status.ts maps the provider to its key name.
AI_MODEL overrides the default model, but only for the provider AI_PROVIDER selects — it does not switch providers.
AI_FALLBACK_ENABLED takes "true" or "false". Cross-provider fallback is on unless this is exactly "false"; lib/ai/model.ts reads it from process.env.
With no key for the selected provider, isProviderConfigured returns false and the AI surfaces stay hidden.
The autonomy ceiling and per-tool switches are in Settings → AI, not in the environment.
Sign-in with Google
AUTH_GOOGLE_ID and AUTH_GOOGLE_SECRET register the Google provider in lib/auth.ts, and they do so only when both are present.
With neither, sign-in is magic link only.
Storage details
Two optional variables adjust how the S3 credentials are used.
S3_ENDPOINT is set for R2 or MinIO, and omitted for AWS.
S3_PUBLIC_URL is the public base URL for served media, for when it differs from the bucket endpoint.
Analytics and tracking
There are two sets here, and they are not interchangeable.
The NEXT_PUBLIC_* values fire browser pixels, and mount only after the shopper accepts the matching consent category:
NEXT_PUBLIC_PLAUSIBLE_DOMAIN,NEXT_PUBLIC_PLAUSIBLE_URLNEXT_PUBLIC_POSTHOG_API_KEY,NEXT_PUBLIC_POSTHOG_HOSTNEXT_PUBLIC_GA_MEASUREMENT_IDNEXT_PUBLIC_FACEBOOK_PIXELNEXT_PUBLIC_TIKTOK_PIXELNEXT_PUBLIC_CURRENCY_API_KEYfor currency rates
The server-side keys do different work:
GA4_MEASUREMENT_IDwithGA4_API_SECRET, andMETA_PIXEL_IDwithMETA_ACCESS_TOKEN, forward events server-to-serverPLAUSIBLE_API_KEY,POSTHOG_API_KEY,POSTHOG_PROJECT_IDread stats back into the admin
A store can configure either side, both, or neither.
Newsletter providers
Configure one of these — the first one configured wins:
- Klaviyo —
KLAVIYO_API_KEY,KLAVIYO_LIST_ID - Mailchimp —
MAILCHIMP_API_KEY,MAILCHIMP_LIST_ID - ConvertKit —
CONVERTKIT_API_KEY,CONVERTKIT_FORM_ID - Buttondown —
BUTTONDOWN_API_KEY - Loops —
LOOPS_API_KEY - Beehiiv —
BEEHIIV_API_KEY,BEEHIIV_PUBLICATION_ID
NEWSLETTER_WEBHOOK_URL posts subscribers to any other platform.
All of this is separate from the Resend transactional path.
Integrations and observability
The Shopify Admin OAuth catalog import needs five values:
SHOPIFY_APP_URL
SHOPIFY_API_KEY
SHOPIFY_API_SECRET
SHOPIFY_SCOPES
SHOPIFY_API_VERSIONTwo variables have length requirements rather than format ones.
SERVICE_API_KEY is a static service key for server-to-server calls, and must be at least 32 characters.
ENCRYPTION_KEY encrypts stored credentials and API keys. It is exactly 64 characters — generate one with openssl rand -hex 32.
RESEND_WEBHOOK_SECRET is the Svix signing secret for Resend delivery events. Without it, /api/webhooks/resend answers 503, and bounces and complaints are not ingested.
ADMIN_ALERT_EMAIL is where admin alert emails are sent, and JINA_API_KEY covers content scraping.
Error reporting uses NEXT_PUBLIC_SENTRY_DSN, SENTRY_ORG, SENTRY_PROJECT and SENTRY_AUTH_TOKEN. next.config.ts applies withSentryConfig only on a production Vercel build, and uploads source maps only in CI.
ENABLE_TAXES is false by default. Off means getTaxProvider returns the zero-tax provider and no order is charged tax.
Inngest is not in the schema
env.ts declares no Inngest variable, and services/inngest.ts constructs the client with an id and a Prisma middleware only.
The SDK picks up INNGEST_EVENT_KEY and INNGEST_SIGNING_KEY from process.env itself, so a missing key fails at send time rather than at startup.
In development that is deliberate. isInlineFallbackEnabled in events/emitter.ts treats NODE_ENV=development with no INNGEST_EVENT_KEY as the signal to run the domain-event handlers inline instead of publishing them.
Verifying your configuration
Open Settings → Developer in the admin.
server/admin/settings/self-host-readiness.ts runs a live SELECT 1 against Postgres and a redis.ping(), each with a 3-second timeout.
It then reports on the public site URL being https, Resend, S3, Stripe and the AI provider. The first five are marked critical; Stripe and AI are not.
Where a setting belongs
Put a value in the environment when it is a credential or an endpoint — something that identifies an external service or authorises a call to it.
Put it in Settings in the admin when it is a business decision: site, appearance, checkout, payments, taxes, delivery, fulfillment, SEO, roles, alerts, notifications, integrations and AI all live there.
The dividing line is not sensitivity, it is who changes it and how often:
Environment variables need a deploy to change; admin settings do not.