Environment Variables Complete reference of every env var Launchy reads and what happens if you leave each blank.
Launchy validates env vars at startup using Zod schemas in lib/env.ts . If a required var is missing or invalid, the app logs the error and exits.
Variable Format Purpose DATABASE_URLpostgresql://...Postgres connection string BETTER_AUTH_SECRET≥ 32 chars Session cookie signing. openssl rand -base64 32 BETTER_AUTH_URLFull URL Used for OAuth callbacks
Variable If blank GOOGLE_CLIENT_ID"Sign in with Google" button hidden GOOGLE_CLIENT_SECRETsame GITHUB_CLIENT_ID"Sign in with GitHub" button hidden GITHUB_CLIENT_SECRETsame
Variable If blank R2_ACCOUNT_IDUpload falls back to public/uploads/ on local filesystem R2_ACCESS_KEY_IDsame R2_SECRET_ACCESS_KEYsame R2_BUCKET_NAMEsame R2_PUBLIC_URLsame NEXT_PUBLIC_CDN_HOSTNAMEnext/image won't allow your CDN hostname — images throw "hostname not allowed" in prod. Not Zod-validated — a typo fails silently.
Variable If blank STRIPE_SECRET_KEYPaid tiers + sponsor checkout disabled STRIPE_WEBHOOK_SECRETWebhook returns 400 — payments succeed but fulfillment never runs (orders created in Stripe, not in your DB)
⚠️ If you set STRIPE_SECRET_KEY, you must also set STRIPE_WEBHOOK_SECRET. Otherwise customers will pay and your app won't register the payment.
Variable Default If blank REDIS_HOST127.0.0.1Rate limiting disabled REDIS_PORT6379— REDIS_PASSWORDempty — REDIS_DB0—
In development (NODE_ENV !== "production"), rate limiting is disabled regardless of Redis config — this is hardcoded in lib/rate-limit.ts.
Variable If blank PLUNK_SECRET_KEYEmail sending returns null silently (with a console warning) PLUNK_PUBLIC_KEYSubscribe endpoint may not work PLUNK_FROM_EMAILsame
Variable If blank CRON_SECRETCron routes always return 401
Generate: openssl rand -base64 32.
Variable If blank NEXT_PUBLIC_TURNSTILE_SITE_KEYCaptcha widget not rendered on auth pages TURNSTILE_SECRET_KEYBetter Auth captcha plugin not enabled
Variable If blank DISCORD_WEBHOOK_URLAdmin Discord notifications silenced
Variable If blank NEXT_PUBLIC_PLAUSIBLE_DOMAINTracking script not rendered — no analytics PLAUSIBLE_CUSTOM_DOMAINUses plausible.io (default). Set if you self-host Plausible.
Variable Purpose STRIPE_TEMPLATE_WEBHOOK_SECRETWebhook secret for the template sale webhook GITHUB_TEMPLATE_PATGitHub token for adding customers as collaborators GITHUB_TEMPLATE_ORGGitHub org name GITHUB_TEMPLATE_REPOGitHub repo name
These are present on the sales site, not in the customer distribution .
Variable If blank TEST_EMAILScripts fall back to a placeholder recipient
File .env at project root. Loaded automatically.
Project → Settings → Environment Variables. Set per environment (Production / Preview / Development). Vercel's Import .env file button speeds this up.
Either env_file: .env in compose, or pass each var via environment:. Keep .env outside the image — never COPY .env in Dockerfile.
Never commit .env — .gitignore excludes it
Rotate on leak :
BETTER_AUTH_SECRET → regenerate, all sessions invalidate
CRON_SECRET → regenerate, update your cron provider
STRIPE_* → regenerate in Stripe Dashboard
R2_* → revoke + reissue API token in Cloudflare
Least-privilege tokens — R2 scoped to one bucket; GitHub PAT scoped to one repo
Different secrets per environment — local, preview, prod must have distinct values
If validation fails at startup, you'll see:
❌ Invalid environment variables:
DATABASE_URL: DATABASE_URL is required
BETTER_AUTH_SECRET: BETTER_AUTH_SECRET must be at least 32 characters
Check your .env file against .env.example
No partial start — fix and re-run.