6 min read

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

Securie findingcritical
.env.local:12

Vercel 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.

Suggested fix — ready as a PR
# 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.
Catch this in my repo →Securie scans every PR · ships the fix as a one-click merge · free during early access

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