Create a .env.local file in your project root. For a first local run, prefer npm run init; it writes the same contract from guided prompts and avoids missing required values.

Watch the locale separator

NEXT_PUBLIC_DEFAULT_LOCALE uses a hyphen (BCP 47): en-US, fr-FR. If your .env.example ever shows en_US (with underscore), that is a typo — i18n/config.ts only matches hyphenated ids.

Environment by scenario

Use this table before opening the full env list. It tells you which variables matter for the stage you are working on.

Scenario Required values Notes
Minimal local app NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, SUPABASE_SECRET_KEY, legal metadata, email sender values Use EMAIL_PROVIDER=noop if you only want to render the app before wiring real email delivery.
Authentication EMAIL_PROVIDER, provider API keys, EMAIL_FROM_NAME, EMAIL_FROM_ADDRESS, optional NEXT_PUBLIC_OAUTH_PROVIDERS, OTP_LENGTH OTP_LENGTH must match the Supabase Auth Email OTP length setting.
Stripe billing STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET, price IDs in config/pricing.ts Plans, credit packs, licenses, currencies, and Stripe Price IDs live in config/pricing.ts, not in the database.
AI / RAG At least one of OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_AI_API_KEY OpenAI is required when using the default RAG embedding flow. Credits are decremented from provider-reported token usage.
B2B workspaces businessModel: 'b2b' in config/app.ts, workspace settings in config/workspace.ts Every B2B subscription targets a workspace account. New users get a workspace through the automatic B2B bootstrap; /onboarding/workspace is a fallback.
Production monitoring LOGS_ENABLED, LOGS_RETENTION_DAYS, LOGS_SALT, JOBS_SECRET_KEY, Upstash Redis values Set salts and secrets in your hosting platform, not in committed files. Logs return 404 while disabled.
Analytics / marketing NEXT_PUBLIC_GTM_ID, NEXT_PUBLIC_GA_MEASUREMENT_ID, Google Ads / Meta / X / Crisp IDs as needed All third-party analytics and live-chat scripts are consent-gated by default.

Variables required for a production-ready boot. Many are still useful in local dev (Supabase, email sender values); the Notes column flags which gate each variable controls so you can skip the ones that are not relevant to your stage.

VariableRequired forDescription
NEXT_PUBLIC_APP_URLapp bootCanonical site origin. Read in config/app.ts as appConfig.url; used by Stripe redirects, sitemap, CSRF Origin/Referer check, and absolute-URL generation. Use the production domain (e.g. https://example.com); for local dev http://localhost:3777 is fine.
NEXT_PUBLIC_DEFAULT_LOCALEapp bootBCP-47 default locale (hyphen, not underscore). Must be one of the configured locales in i18n/config.ts (default: en-US). Drives the locale auto-detect fallback in proxy.ts and the sitemap priority multiplier.
NEXT_PUBLIC_SUPABASE_URLapp bootYour Supabase project URL (Settings → API)
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEYapp bootSupabase publishable key for client-side access
SUPABASE_SECRET_KEYserver / RLS bypassSupabase secret key for server-side operations (never expose to client)
SUPABASE_POSTGRES_URLinit scriptDirect Postgres connection string consumed by scripts/init-project.js to run schema/migration SQL and seed jobs. Find it under Settings → Database → Connection string → URI in the Supabase Dashboard. Not used at request time.
STRIPE_SECRET_KEYbillingStripe secret key for payment processing
STRIPE_WEBHOOK_SECRETbillingWebhook signing secret from Stripe Dashboard
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYbillingStripe publishable key for client-side Checkout
CONTACT_EMAILapp bootContact-form inbox. Surfaced in the footer and used as the To address for messages submitted on /contact. Confirmation copies and admin templates also read this value via appConfig.contact.email.
SUPPORT_EMAILapp bootSupport inbox shown to logged-in users (My Account, error pages, license expiration notices). Defaults to CONTACT_EMAIL when the init wizard runs without an explicit value.
EMAIL_PROVIDERauth / transactional emailActive email provider — brevo | mailjet | noop (default: brevo). Use noop for dev / CI when you don't want to deliver anything.
EMAIL_FROM_NAMEauth / transactional emailSender name for outgoing emails
EMAIL_FROM_ADDRESSauth / transactional emailSender email address (must be verified with the active provider)
BREVO_API_KEYwhen EMAIL_PROVIDER=brevoBrevo API key
BREVO_NEWSLETTER_LIST_IDwhen EMAIL_PROVIDER=brevoBrevo contact list id for newsletter
MAILJET_API_KEY_PUBLICwhen EMAIL_PROVIDER=mailjetMailjet public key
MAILJET_API_KEY_PRIVATEwhen EMAIL_PROVIDER=mailjetMailjet private/secret key
MAILJET_NEWSLETTER_LIST_IDoptional (Mailjet)Mailjet contact list id for newsletter
LEGAL_COMPANY_NAMEpublic/indexable buildsLegal company name shown on public legal pages. Required before enabling public/indexable production builds.
LEGAL_ADDRESSpublic/indexable buildsRegistered company address shown on public legal pages. Required before enabling public/indexable production builds.
LEGAL_REGISTRATION_NUMBERpublic/indexable buildsCompany registration number (RCS, SIRET, UID, etc.). Required before enabling public/indexable production builds.

These variables enable additional features but are not required to start:

VariableDescription
OPENAI_API_KEYOpenAI API key for GPT models
ANTHROPIC_API_KEYAnthropic API key for Claude models
GOOGLE_AI_API_KEYGoogle AI API key for Gemini models
NEXT_PUBLIC_TURNSTILE_SITE_KEYCloudflare Turnstile site key for bot protection
TURNSTILE_SECRET_KEYCloudflare Turnstile server-side secret
NEXT_PUBLIC_GTM_IDGoogle Tag Manager container ID (GTM-XXX)
NEXT_PUBLIC_GA_MEASUREMENT_IDGoogle Analytics 4 Measurement ID (G-XXX) — primary ID for gtag.js
NEXT_PUBLIC_GOOGLE_ADS_IDGoogle Ads ID for conversion tracking (e.g. AW-XXXXXXXXXXX)
NEXT_PUBLIC_GOOGLE_ADS_CONVERSION_LABELLegacy default Google Ads conversion label (fallback)
NEXT_PUBLIC_GOOGLE_ADS_CONVERSION_PURCHASEGoogle Ads purchase conversion tag (format: AW-XXX/LABEL)
NEXT_PUBLIC_GOOGLE_ADS_CONVERSION_SIGNUPGoogle Ads signup conversion tag (format: AW-XXX/LABEL)
NEXT_PUBLIC_META_PIXEL_IDMeta (Facebook) Pixel ID for ad tracking
NEXT_PUBLIC_X_PIXEL_IDX (Twitter) Pixel base ID for ad tracking
NEXT_PUBLIC_X_PIXEL_EVENT_PURCHASEX Pixel purchase conversion event ID (format: tw-XXX-XXX)
NEXT_PUBLIC_X_PIXEL_EVENT_SIGNUPX Pixel signup conversion event ID (format: tw-XXX-XXX)
NEXT_PUBLIC_CRISP_WEBSITE_IDCrisp live chat website ID
UPSTASH_REDIS_REST_URLUpstash Redis URL for rate limiting (falls back to in-memory)
UPSTASH_REDIS_REST_TOKENUpstash Redis authentication token
TRUST_CLOUDFLARE_IPSet to true only when the origin is locked behind Cloudflare or your edge strips spoofed proxy headers. When disabled, rate limiting ignores spoofable CF-Connecting-IP headers and falls back to proxy headers such as X-Forwarded-For.
JOBS_SECRET_KEYSecret key for authenticating cron and webhook job triggers
DEFAULT_TRIAL_PERIOD_DAYSDefault free trial length in days for subscription plans configured with trialDays. Unset/blank → 7. 0 disables trials. Clamped to 730 (Stripe max).
DEFAULT_TRIAL_CREDITSCredits granted up-front when a subscription enters status='trialing' so the user can use the app during the trial. The first paid invoice tops up the difference (full plan allotment − trial amount). Capped at the plan's included credits. Unset/0 → trialing users get nothing until the trial converts.
PRELAUNCH_ALLOWED_EMAILSServer-only. Comma-separated emails allowed to complete sign-in (magic link + OAuth) while NEXT_PUBLIC_PRELAUNCH=true. Case-insensitive. Empty = sign-in fully locked. See Sign-In Allowlist.
NEXT_PUBLIC_OAUTH_PROVIDERSComma-separated provider ids rendered as buttons on /login (e.g. google,github,apple). Each id must also be enabled in the Supabase Auth dashboard. Unknown ids drop silently. See supported ids. Default: google.
OTP_LENGTHServer-only. Length of the magic-link 6-digit code (range 6–10, default 6). MUST match the project's Authentication → Email OTP length setting in the Supabase dashboard — Supabase generates the actual code; this env var tells our UI/API how many digits to expect. Read once in lib/env.ts; exposed as appConfig.auth.otpLength. See Configuring the code length.

Copy the .env.example file to .env.local and fill in all values. The example file contains every variable with descriptive comments explaining each one. Variables prefixed with NEXT_PUBLIC_ are exposed to the browser and should only contain non-sensitive values. All other variables remain server-side only.

Feature Flags

Set NEXT_PUBLIC_ONBOARDING_ENABLED=true to enable the onboarding flow after first login. Set NEXT_PUBLIC_PRELAUNCH=false to disable prelaunch/waitlist mode. Set NEXT_PUBLIC_INDEXABLE=true to allow search engine indexing (defaults to blocked when not set).

Feature-gate & secret env vars

These power optional domains and are read once in their config file (never process.env in components). Server-only salts are required in production — a missing salt disables hashing, not the feature.

VariablePurpose
REFERRAL_ENABLED / REFERRAL_SALTReferral program gate (404 when off) + IP/UA hash salt. Read in config/referral.ts.
AFFILIATES_ENABLED / AFFILIATES_SALTAffiliate program gate + hash salt (distinct from REFERRAL_SALT). Read in config/affiliates.ts.
LOGS_ENABLED / LOGS_RETENTION_DAYS / LOGS_SALTError-log dashboard gate, purge window (default 90), IP hash salt. Read in config/app.ts.
TRUST_CLOUDFLARE_IPRate-limiter trust knob for Cloudflare's CF-Connecting-IP header. Keep false unless direct origin access is blocked or the trusted proxy strips spoofed headers.
VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEYWeb Push keys (server-only, no NEXT_PUBLIC_). Generate with npx web-push generate-vapid-keys.
NEXT_PUBLIC_PUSH_ENABLEDToggles the PWA push UI.
OTP_LENGTHMagic-link code length (6–10, default 6). MUST match the Supabase Auth Email-OTP-length setting.
LEGAL_COMPANY_NAME / LEGAL_ADDRESS / LEGAL_REGISTRATION_NUMBERPublic legal metadata read in config/app.ts. Example placeholders block public/indexable production builds.
NEXT_PUBLIC_GTM_ID, NEXT_PUBLIC_GA_MEASUREMENT_ID, NEXT_PUBLIC_META_PIXEL_ID, NEXT_PUBLIC_X_PIXEL_ID, NEXT_PUBLIC_GOOGLE_ADS_ID, NEXT_PUBLIC_CRISP_WEBSITE_IDAnalytics / live-chat integrations (consent-aware). Validated in lib/env.ts.
NEXT_PUBLIC_ONBOARDING_ENABLED, NEXT_PUBLIC_PRELAUNCH, NEXT_PUBLIC_INDEXABLEOnboarding flow, prelaunch/waitlist mode, search-engine indexing gate.
NEXT_PUBLIC_INSTANCE_MODEDeployment mode flag shipped in .env.example (e.g. development / production). Set per environment.
Keep .env.example and init in sync

.env.example and npm run init declare the supported env contract. When adding a new public knob or server-only secret, update both the example file and the init script so new projects do not miss required runtime configuration.

OAuth supported ids

NEXT_PUBLIC_OAUTH_PROVIDERS is a comma-separated list of Supabase provider ids rendered as buttons on /login. Each id must also be enabled in the Supabase Auth dashboard; unknown ids drop silently.

Supported ids (matching the Supabase Provider type): apple, azure, bitbucket, discord, facebook, figma, github, gitlab, google, kakao, keycloak, linkedin (legacy), linkedin_oidc, notion, slack (legacy), slack_oidc, spotify, twitch, twitter. Empty value = magic link only. Default: google.

Configuring the magic-link code length

OTP_LENGTH is the number of digits in the 6-digit-style code embedded in magic-link emails. Range: 6–10, default 6. Server-only (no NEXT_PUBLIC_ prefix). MUST match the value of Authentication → Email OTP length in the Supabase dashboard — Supabase generates the code, this env var tells our UI/API how many digits to validate. Read once in lib/env.ts; exposed elsewhere as appConfig.auth.otpLength.

Sign-in allowlist (prelaunch mode)

While NEXT_PUBLIC_PRELAUNCH=true, sign-in is gated server-side by PRELAUNCH_ALLOWED_EMAILS — a comma-separated allowlist (case-insensitive). The magic-link API blocks non-allowlisted emails before calling admin.generateLink, and the OAuth callback signs the user out and deletes the auto-created auth user if their email is not on the list. Leave empty to fully lock sign-in during the prelaunch window. Flip NEXT_PUBLIC_PRELAUNCH to false to disable the gate entirely.