Vercel Environment Variables: Scoping and Secrets Guide

By Yanni Papoutsis | 10 min read | 2026-07-27

TL;DR: Vercel environment variables have three scopes, Production, Preview and Development, and understanding which applies when removes most of the confusion people have with them. Production applies to deployments from your production branch. Preview applies to every other branch and every pull request deployment. Development applies when you pull variables down for local work. A variable can be assigned to any combination of the three, and the mistake that costs the most time is setting a variable in Production only and then wondering why preview deployments fail. The second axis is exposure: in Next.js, anything prefixed NEXT_PUBLIC_ is inlined into the client bundle at build time and is readable by anyone. Never put a secret behind that prefix. The third axis is timing: a value inlined at build time is frozen into the output, while a server-side value is read fresh at request time. Get the three axes straight and Vercel's variable system becomes predictable.

What are the three environments and when does each apply?

Vercel gives every variable a set of environments it belongs to. The names are self-explanatory but the boundaries are where people slip.

Production applies to deployments created from your production branch, usually main, and to anything you explicitly promote to production. This is what your live domain serves.

Preview applies to deployments from every other branch. Open a pull request and you get a preview deployment with its own URL, built with the Preview variable set. This is the environment most people forget to configure.

Development applies when you pull variables to your local machine to run the project locally. It is not used by any deployment; it exists so your local environment can match without you maintaining a separate .env by hand.

A variable can belong to one, two or all three. The dashboard presents this as a set of checkboxes and the CLI as flags. The important consequence: assigning a variable to Production does not assign it to Preview. They are separate. A preview build looking for DATABASE_URL that was only set in Production sees nothing at all, and the resulting error usually points at your code rather than at the missing variable.

Vercel also supports branch-specific values within Preview, so a staging branch can hold different values from other preview branches. Use that when you have a long-lived staging branch that needs its own backend, and avoid it otherwise, because per-branch values are easy to set and easy to forget.

Which variables end up in the browser?

This is the rule that matters most, because getting it wrong publishes a secret.

In Next.js, any variable whose name begins with NEXT_PUBLIC_ is inlined into the JavaScript bundle at build time. It is not sent to the browser at runtime from a server, it is compiled into the code. Anyone can open dev tools and read it. It is also permanently baked into that build, so changing it in the dashboard has no effect until you rebuild.

Everything without that prefix is available only to server-side code: server components, route handlers, API routes, server actions, and the build process itself. Referencing it from a client component yields undefined.

# Safe: these are meant to be public, and the prefix makes that explicit
NEXT_PUBLIC_SITE_URL=https://your-site.example.com
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=REPLACE_ME_ANON_KEY

# NEVER give these the NEXT_PUBLIC_ prefix
SUPABASE_SERVICE_ROLE_KEY=REPLACE_ME
STRIPE_SECRET_KEY=sk-REPLACE_ME
GITHUB_TOKEN=YOUR_GITHUB_TOKEN
DATABASE_URL=postgresql://user:REPLACE_ME@host:5432/dbname

The Supabase anon key is a deliberate example of the distinction. It is designed to be public and is safe in a bundle, provided row level security is actually enabled on your tables. The service role key bypasses row level security entirely and must never leave the server. Two keys from the same provider, opposite handling. The auth patterns in supabase-auth-google-oauth depend on that distinction being respected.

Other frameworks use different prefixes for the same mechanism. Vite uses VITE_, Astro uses PUBLIC_. The principle is universal: a prefix marks a variable as intentionally public, and the moment you use it on something sensitive, that thing is no longer sensitive.

When is a variable read: build time or runtime?

The third axis, and the source of the "I changed it but nothing happened" complaint.

Build-time variables are read while your project builds. Anything inlined into the client bundle is build-time by definition. Values used to generate static pages are build-time. Once the build finishes, those values are frozen into the artefacts. Changing them in the dashboard changes nothing until you redeploy.

Runtime variables are read when a request arrives. Server components, route handlers and serverless functions read process.env at request time, so a dashboard change takes effect on the next request without a rebuild for functions that read them dynamically. In practice Next.js may inline some server values during the build too, depending on how the route is rendered, so do not rely on this without testing.

The rule that avoids trouble: after changing any environment variable, redeploy. It is cheap, it is unambiguous, and it removes an entire class of confusing behaviour. The alternative is reasoning about which of your routes were statically generated, which were server rendered, and which were inlined, for every change.

How do I manage variables from the CLI?

The dashboard is fine for a handful of variables. Beyond that, the CLI is faster and less error-prone.

# Link your local directory to the Vercel project once
vercel link

# Add a variable, choosing environments interactively
vercel env add DATABASE_URL

# Add to a specific environment non-interactively
vercel env add STRIPE_SECRET_KEY production

# List what exists, without printing values
vercel env ls

# Pull the Development set into a local file for local work
vercel env pull .env.local

vercel env pull writes real values to disk. Two consequences follow. First, .env.local must be in .gitignore before you ever run the command. Next.js projects ignore it by default, but confirm rather than assume. Second, the file now contains live credentials on your laptop, so it deserves the same care as any other secret store.

The vercel env ls command is your audit tool. Run it periodically and delete variables you no longer recognise. Environment variables accumulate silently: a key from an integration you tried once, a token for a service you dropped. Each one is a live credential nobody is watching.

What are the secret hygiene rules?

These are not optional and the cost of ignoring them is unbounded.

Never commit .env. Add it to .gitignore before the file exists, not after. The gap between creating the file and ignoring it is where accidents happen.

Never use a public prefix on a secret. NEXT_PUBLIC_STRIPE_SECRET_KEY is not a hypothetical mistake, it is one people make, and it publishes a payment credential to every visitor.

Rotate on any exposure, always. If a key has been committed, pasted into a chat, shown in a screenshot, or printed in a build log, treat it as compromised. Git history is not erasable in any meaningful sense: forks, mirrors, caches and clones all retain it. Revoking and reissuing takes minutes. Not doing so leaves a live credential in the wild indefinitely.

Scope keys to the minimum. A read-only database role for a service that only reads. A restricted API key rather than a full one. A token limited to one repository rather than every repository you can see, which is the reasoning set out in connect-github-to-claude.

Use different values per environment. Preview deployments should not hold production credentials. A preview URL is public, a preview build runs code from a branch that has not been reviewed, and giving that combination production database access is an obvious hazard once you say it out loud.

Never log a variable. console.log(process.env) in a serverless function writes every secret to your logs, where they are retained and readable by anyone with dashboard access.

The general framing in skill-security-best-practices applies directly: assume anything that leaves a controlled environment is compromised, and build so that rotation is a routine operation rather than an incident.

How should preview environments be configured?

Deliberately, and differently from production.

Point Preview at a staging backend. A separate database, separate API keys, test-mode payment credentials. The effort is a one-time setup and it means a bad branch cannot damage production data.

For payment providers, this is not a nicety. Use test keys in Preview. A preview deployment with live payment keys can create real charges, and the fact that nobody meant to is not a defence.

Consider protecting preview URLs. They are public by default, they can be indexed if anything links to them, and they may contain unreleased work. If your previews show anything you would not put on the open web, restrict access to them.

And check that Preview variables exist at all. The failure mode is a preview build that errors on a missing variable and sends you looking for a code bug that is not there.

How do I debug a variable that is not working?

Work through this in order.

  1. Is it assigned to the environment you are testing? Open the dashboard or run vercel env ls and check the environment column. This is the answer roughly half the time.
  2. Have you redeployed since changing it? Build-time values are frozen into the artefact. If you did not redeploy, you are testing the old value.
  3. Are you reading it from the right side? A variable without a public prefix is undefined in client components. That is correct behaviour, not a bug.
  4. Is the name exactly right? Environment variables are case-sensitive and a trailing space in the dashboard is invisible and fatal.
  5. Is something else overriding it? A local .env.local overrides pulled values. A variable set at both project and team level may resolve differently from what you expect.
  6. Check the build log. It shows which variables were available during the build, by name. If yours is not listed, the problem is assignment rather than code.

The same class of confusion appears on other platforms with different names. The Cloudflare equivalent, where Production and Preview split the same way but the runtime reads values from a request context rather than process.env, is covered in deploy-astro-cloudflare-pages. If you are setting up the deployment itself rather than debugging its configuration, deploy-react-app-vercel covers the ground before this one.

Frequently Asked Questions

Why does my environment variable work in production but not in preview deployments?

Because Production and Preview are separate scopes and assigning a variable to one does not assign it to the other. This is the most common Vercel environment variable problem by a wide margin. Open the variable in the dashboard and check which environment checkboxes are ticked, or run vercel env ls and read the environment column. Set the variable for Preview as well, ideally with a staging value rather than the production one, and redeploy the branch.

Is the NEXT_PUBLIC prefix safe for API keys?

Only for keys that are designed to be public, such as a Supabase anon key protected by row level security or a publishable payment key. The prefix inlines the value into the JavaScript bundle at build time, where anyone can read it in dev tools. A secret key, a service role key or a personal access token behind that prefix is published to every visitor. If you have ever deployed a secret with a public prefix, rotate it immediately: removing the prefix does not recall the bundles already served.

I changed a variable in the dashboard and nothing happened. Why?

Build-time variables are compiled into the deployment artefact, so the running deployment still holds the old value regardless of what the dashboard now says. Every value inlined into a client bundle and every value used to generate a static page falls into this category. Redeploy after any environment variable change. It is the simplest rule and it avoids having to reason about which routes were statically generated and which read process.env at request time.

Should preview deployments use production credentials?

No. Preview URLs are publicly accessible, previews run unreviewed code from feature branches, and giving that combination access to your production database or live payment keys is a genuine risk rather than a theoretical one. Point Preview at a staging database and test-mode payment credentials. If a preview must reach something production-like, use a read-only credential scoped as narrowly as the work allows, and restrict access to the preview URLs themselves.

What do I do if I have committed a secret to the repository?

Rotate it. Not later, now. Revoke the key at the provider and issue a new one, then update the value in Vercel. Do not rely on rewriting Git history: the commit may already exist in forks, clones, CI caches, backups and third-party mirrors, and none of those are under your control. Removing the file makes the repository tidy without making the credential safe. Treat rotation as the whole response and the history cleanup as optional housekeeping afterwards.