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.
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.
| Variable | Required for | Description |
|---|---|---|
NEXT_PUBLIC_APP_URL | app boot | Canonical 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_LOCALE | app boot | BCP-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_URL | app boot | Your Supabase project URL (Settings → API) |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY | app boot | Supabase publishable key for client-side access |
SUPABASE_SECRET_KEY | server / RLS bypass | Supabase secret key for server-side operations (never expose to client) |
SUPABASE_POSTGRES_URL | init script | Direct 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_KEY | billing | Stripe secret key for payment processing |
STRIPE_WEBHOOK_SECRET | billing | Webhook signing secret from Stripe Dashboard |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | billing | Stripe publishable key for client-side Checkout |
CONTACT_EMAIL | app boot | Contact-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_EMAIL | app boot | Support 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_PROVIDER | auth / transactional email | Active email provider — brevo | mailjet | noop (default: brevo). Use noop for dev / CI when you don't want to deliver anything. |
EMAIL_FROM_NAME | auth / transactional email | Sender name for outgoing emails |
EMAIL_FROM_ADDRESS | auth / transactional email | Sender email address (must be verified with the active provider) |
BREVO_API_KEY | when EMAIL_PROVIDER=brevo | Brevo API key |
BREVO_NEWSLETTER_LIST_ID | when EMAIL_PROVIDER=brevo | Brevo contact list id for newsletter |
MAILJET_API_KEY_PUBLIC | when EMAIL_PROVIDER=mailjet | Mailjet public key |
MAILJET_API_KEY_PRIVATE | when EMAIL_PROVIDER=mailjet | Mailjet private/secret key |
MAILJET_NEWSLETTER_LIST_ID | optional (Mailjet) | Mailjet contact list id for newsletter |
LEGAL_COMPANY_NAME | public/indexable builds | Legal company name shown on public legal pages. Required before enabling public/indexable production builds. |
LEGAL_ADDRESS | public/indexable builds | Registered company address shown on public legal pages. Required before enabling public/indexable production builds. |
LEGAL_REGISTRATION_NUMBER | public/indexable builds | Company registration number (RCS, SIRET, UID, etc.). Required before enabling public/indexable production builds. |
These variables enable additional features but are not required to start:
| Variable | Description |
|---|---|
OPENAI_API_KEY | OpenAI API key for GPT models |
ANTHROPIC_API_KEY | Anthropic API key for Claude models |
GOOGLE_AI_API_KEY | Google AI API key for Gemini models |
NEXT_PUBLIC_TURNSTILE_SITE_KEY | Cloudflare Turnstile site key for bot protection |
TURNSTILE_SECRET_KEY | Cloudflare Turnstile server-side secret |
NEXT_PUBLIC_GTM_ID | Google Tag Manager container ID (GTM-XXX) |
NEXT_PUBLIC_GA_MEASUREMENT_ID | Google Analytics 4 Measurement ID (G-XXX) — primary ID for gtag.js |
NEXT_PUBLIC_GOOGLE_ADS_ID | Google Ads ID for conversion tracking (e.g. AW-XXXXXXXXXXX) |
NEXT_PUBLIC_GOOGLE_ADS_CONVERSION_LABEL | Legacy default Google Ads conversion label (fallback) |
NEXT_PUBLIC_GOOGLE_ADS_CONVERSION_PURCHASE | Google Ads purchase conversion tag (format: AW-XXX/LABEL) |
NEXT_PUBLIC_GOOGLE_ADS_CONVERSION_SIGNUP | Google Ads signup conversion tag (format: AW-XXX/LABEL) |
NEXT_PUBLIC_META_PIXEL_ID | Meta (Facebook) Pixel ID for ad tracking |
NEXT_PUBLIC_X_PIXEL_ID | X (Twitter) Pixel base ID for ad tracking |
NEXT_PUBLIC_X_PIXEL_EVENT_PURCHASE | X Pixel purchase conversion event ID (format: tw-XXX-XXX) |
NEXT_PUBLIC_X_PIXEL_EVENT_SIGNUP | X Pixel signup conversion event ID (format: tw-XXX-XXX) |
NEXT_PUBLIC_CRISP_WEBSITE_ID | Crisp live chat website ID |
UPSTASH_REDIS_REST_URL | Upstash Redis URL for rate limiting (falls back to in-memory) |
UPSTASH_REDIS_REST_TOKEN | Upstash Redis authentication token |
TRUST_CLOUDFLARE_IP | Set 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_KEY | Secret key for authenticating cron and webhook job triggers |
DEFAULT_TRIAL_PERIOD_DAYS | Default free trial length in days for subscription plans configured with trialDays. Unset/blank → 7. 0 disables trials. Clamped to 730 (Stripe max). |
DEFAULT_TRIAL_CREDITS | Credits 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_EMAILS | Server-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_PROVIDERS | Comma-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_LENGTH | Server-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.
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.
| Variable | Purpose |
|---|---|
REFERRAL_ENABLED / REFERRAL_SALT | Referral program gate (404 when off) + IP/UA hash salt. Read in config/referral.ts. |
AFFILIATES_ENABLED / AFFILIATES_SALT | Affiliate program gate + hash salt (distinct from REFERRAL_SALT). Read in config/affiliates.ts. |
LOGS_ENABLED / LOGS_RETENTION_DAYS / LOGS_SALT | Error-log dashboard gate, purge window (default 90), IP hash salt. Read in config/app.ts. |
TRUST_CLOUDFLARE_IP | Rate-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_KEY | Web Push keys (server-only, no NEXT_PUBLIC_). Generate with npx web-push generate-vapid-keys. |
NEXT_PUBLIC_PUSH_ENABLED | Toggles the PWA push UI. |
OTP_LENGTH | Magic-link code length (6–10, default 6). MUST match the Supabase Auth Email-OTP-length setting. |
LEGAL_COMPANY_NAME / LEGAL_ADDRESS / LEGAL_REGISTRATION_NUMBER | Public 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_ID | Analytics / live-chat integrations (consent-aware). Validated in lib/env.ts. |
NEXT_PUBLIC_ONBOARDING_ENABLED, NEXT_PUBLIC_PRELAUNCH, NEXT_PUBLIC_INDEXABLE | Onboarding flow, prelaunch/waitlist mode, search-engine indexing gate. |
NEXT_PUBLIC_INSTANCE_MODE | Deployment mode flag shipped in .env.example (e.g. development / production). Set per environment. |
.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.