Vercel environment variables security — the rules and the leaks
Vercel environment variables have three flavors (development, preview, production) and two scopes (server-only and NEXT_PUBLIC_). Mixing them up leaks production secrets. Here is the rule and the canonical bugs.
Vercel's env-var system is more powerful and more dangerous than `.env` files. Variables are scoped per-environment (dev / preview / production) and can be marked client-exposed (`NEXT_PUBLIC_`). Most leaks come from one of three patterns: prod secret in a NEXT_PUBLIC_ var, prod secret accessible from preview deploys, or env-var leaking through a build log. Here is the rule that prevents all three.
What it is
Vercel injects environment variables into your Next.js builds at three points: locally during `vercel dev`, during preview deployments (every PR gets a unique preview URL), and during production deployments. Each variable can be scoped to one or more of these environments via the Vercel dashboard. Variables prefixed with `NEXT_PUBLIC_` are inlined into the client JavaScript bundle and are visible to any browser that loads the page.
Vulnerable example
# Vercel dashboard — Environment Variables (the wrong way)
# Variable name | Value | Environments
# --------------------------------|----------------|-----------------------------
# NEXT_PUBLIC_OPENAI_API_KEY | sk-proj-... | Production, Preview, Dev
# STRIPE_SECRET_KEY | sk_live_... | Production, Preview, Dev
# DATABASE_URL | postgres://... | Production, Preview, Dev
# Three bugs:
# 1. NEXT_PUBLIC_OPENAI_API_KEY ships in the client bundle — every visitor reads it.
# 2. STRIPE_SECRET_KEY in Preview = production Stripe key on every PR's preview URL.
# 3. DATABASE_URL in Preview = preview deploys writing to production database.Fixed example
# Vercel dashboard — Environment Variables (the right way)
# Variable name | Value (prod) | Value (preview/dev) | Environments
# -------------------------------|----------------|---------------------|-----------
# OPENAI_API_KEY | sk-proj-... | sk-proj-test-... | Production / Preview+Dev
# STRIPE_SECRET_KEY | sk_live_... | sk_test_... | Production / Preview+Dev
# DATABASE_URL | <prod url> | <staging url> | Production / Preview+Dev
# Rules:
# 1. NO secret has NEXT_PUBLIC_ prefix. Ever.
# 2. Production secrets only set in Production environment scope.
# 3. Preview / Dev have separate, lower-privilege values (Stripe test mode,
# staging database, throwaway API keys with low rate limits).
# 4. Use the Vercel CLI or dashboard's per-environment-scope feature, never
# one value across all three environments for production secrets.How Securie catches it
.env.local:12Vercel environment variables security
Securie's secret_scanner specialist runs on every PR and detects: (1) any secret pattern in a NEXT_PUBLIC_ variable in `next.config.js` or env-var defaults; (2) any production-pattern secret (sk_live_, AKIA, prod-database URL) in a non-production environment file. The Vercel integration (R5-T2/T4) reads the project's env-var configuration via the Vercel API and flags scope mismatches at deploy-gate time. Auto-rotation (Indie tier and up) handles the response when a leak is detected.
# Vercel dashboard — Environment Variables (the right way)
# Variable name | Value (prod) | Value (preview/dev) | Environments
# -------------------------------|----------------|---------------------|-----------
# OPENAI_API_KEY | sk-proj-... | sk-proj-test-... | Production / Preview+Dev
# STRIPE_SECRET_KEY | sk_live_... | sk_test_... | Production / Preview+Dev
# DATABASE_URL | <prod url> | <staging url> | Production / Preview+Dev
# Rules:
# 1. NO secret has NEXT_PUBLIC_ prefix. Ever.
# 2. Production secrets only set in Production environment scope.
# 3. Preview / Dev have separate, lower-privilege values (Stripe test mode,
# staging database, throwaway API keys with low rate limits).
# 4. Use the Vercel CLI or dashboard's per-environment-scope feature, never
# one value across all three environments for production secrets.Checklist
- No env var name starts with `NEXT_PUBLIC_` if its value is a secret.
- Production-tier secret values are scoped to Production environment ONLY (not Preview, not Development).
- Preview deploys use staging credentials, not production credentials, for every connected service (Stripe, OpenAI, AWS, database, third-party APIs).
- Build logs are reviewed for accidental `console.log(process.env.SECRET)` patterns — strip these before committing.
- Env vars set via `vercel env pull` are added to `.gitignore` (`.env*.local`).
- When rotating a secret, update Vercel for ALL environments + redeploy ALL three (production, preview, dev) — caching can keep old values alive longer than expected.
FAQ
Why does `NEXT_PUBLIC_` matter? Won't my code only read it server-side anyway?
No — `NEXT_PUBLIC_` causes Next.js to inline the value into the client JavaScript bundle at build time. Any visitor's browser reads the variable from the bundle source. The reason `NEXT_PUBLIC_` exists is for genuinely public values (your Stripe publishable key, your Supabase anon key, your Google Analytics ID). It is never appropriate for secrets.
What if I need a different value per Vercel environment?
Vercel supports per-environment values. In the dashboard: Settings → Environment Variables → set the variable's value separately for Production, Preview, and Development. The CLI: `vercel env add <name> production`, `vercel env add <name> preview`, etc. Each environment reads its own value at build/runtime.
Are preview deploys publicly accessible? Can attackers find them?
Preview URLs are generated from commit SHA + branch + deployment hash and are not in your sitemap, but they are not private — anyone who learns the URL can access them. If your preview deploys connect to production databases or use production secrets, an attacker who learns one preview URL has access to your production. Use staging-equivalent secrets for previews.
How do I verify my env-var configuration is correct?
Run `vercel env pull` against each environment and inspect what comes back. Verify: no production-pattern secrets in preview/dev, no `NEXT_PUBLIC_` prefix on anything that's actually a secret, no commits of `.env*.local`. Securie's Vercel integration automates this audit and runs it on every deploy.
Related guides
The service-role key bypasses every RLS policy you wrote. It exists for a reason; it leaks for many reasons. Here is the rule for when to use it, the patterns that leak it, and the recovery playbook when it does.
Every week founders tweet about their OpenAI bill going from $10 to $10,000 overnight. Usually the cause is an API key committed to a public repo. Here is why it happens in Next.js specifically and how to stop it in five minutes.
Not in .env files. Not in localStorage. Here is the 2026 guide to storing and accessing secrets in a small-team Node.js / Python app.
Rotating an API key without taking your app down requires a specific dual-read single-write sequence. Here is the exact pattern.