compareupdated

NEXT_PUBLIC_ vs VITE_: client env prefixes compared

Both Next.js and Vite inline prefixed env vars at build time. Same idea, different names, slightly different semantics. What to use when, and the gotchas.

TL;DR

  • Both NEXT_PUBLIC_ and VITE_ inline prefixed env vars into the client JS bundle at build time.
  • Same idea, different namespace, slightly different runtime API.
  • Anything without the prefix stays server-only.

The same concept

Modern bundlers can't know at build time which env vars you want exposed to the browser. The convention both Next.js and Vite landed on: require a prefix. Prefixed vars get inlined into the client bundle; un-prefixed ones are invisible to the browser.

Why a prefix instead of opt-in per-variable? Because:

  • It's reviewable in a PR diff (NEXT_PUBLIC_STRIPE_SECRET_KEY is obviously wrong).
  • No hidden config — what you name is what you get.
  • Matches the Unix convention of environment variables as flat strings.

Side-by-side

AspectNext.jsVite
PrefixNEXT_PUBLIC_VITE_
Access in client codeprocess.env.NEXT_PUBLIC_Ximport.meta.env.VITE_X
Access on serverprocess.env.X (all vars)process.env.X (via Node)
Config file.env.local, .env.production.env.local, .env.production
Typed by defaultNo (string only)No (string only)
Build-time or runtime?Build-time (inlined)Build-time (inlined)
Requires rebuild to changeYesYes

The two runtime-API differences that matter

1. process.env vs import.meta.env

Next.js uses process.env.* everywhere — client and server, same syntax. Vite exposes client env through import.meta.env.* (ES-module standard) and the server side reads plain process.env via Node as usual.

Practical impact: if you copy a client component from a Next app to a Vite app, you rename process.env.NEXT_PUBLIC_X → import.meta.env.VITE_X.

2. Vite exposes import.meta.env.MODE

Vite bakes a few built-in values into import.meta.env:

import.meta.env.MODE        // "development" | "production"
import.meta.env.DEV         // boolean
import.meta.env.PROD        // boolean
import.meta.env.BASE_URL    // public base path
import.meta.env.SSR         // boolean

Next.js gives you process.env.NODE_ENV for the same purpose but no first-class flags — you write process.env.NODE_ENV === 'production'.

Shared gotchas

  • Values are inlined at build time. Changing them in your host's dashboard requires a redeploy — both frameworks behave the same way here.
  • Anything prefixed ships to every user's browser. Never prefix sk_live_*, database URLs, or service-role keys. Run the leak checker on your.env before deploying.
  • Comments in .env work in both, but inline comments (KEY=value # note) behave slightly differently in edge cases — quote the value if there's any chance of ambiguity.
  • Testing: Next.js skips .env.local in test mode by default. Vite respects .env.test when NODE_ENV=test. Keep env per mode.

Common mistakes

Using the wrong prefix after a migration

You port a Vite component to Next.js, keep import.meta.env.VITE_API_URL. Next.js doesn't recognize either the import.meta.env syntax or the VITE_ prefix → value is undefined in the browser.

Forgetting the prefix

You add API_URL=... to .env, reference it as process.env.API_URL in a client component, and wonder why it's undefined. Both frameworks silently drop un-prefixed vars from the client bundle.

FAQ

Can I read a NEXT_PUBLIC_ variable from server-side code?

Yes — the prefix only controls what gets inlined into the client bundle. Server components, API routes, and server actions can read both prefixed and unprefixed variables via process.env.

Why does my NEXT_PUBLIC_ or VITE_ variable show as undefined after I added it?

Both frameworks inline these values at build time, so the dev server (or a fresh build) needs to restart after you add or change one — a hot-reload alone isn't enough to pick up a new variable.

Does Nuxt or SvelteKit use the same prefix convention?

The pattern is the same idea, different prefix: Nuxt uses NUXT_PUBLIC_, SvelteKit uses PUBLIC_. All of them solve the identical problem — bundlers can't guess which variables are safe to ship to the browser, so each framework picked an explicit naming convention instead.

Is there a way to avoid rebuilding just to change a public value?

Not with the standard prefix mechanism in either framework — it's inlined at build time by design. If you need a client-visible value that updates without a rebuild, fetch it from an API route or a runtime config service instead of baking it in as an env var.

Which should you pick?

This is really a Next.js vs Vite decision, not a prefix decision. The env variable handling is near-identical. Pick based on whether you want SSR/SSG (Next.js) or a pure SPA build with smaller tooling (Vite).

For a deeper primer on the Next.js side, read What is NEXT_PUBLIC_ in Next.js env variables?

Generator quick links

Stack-specific .env examples:Next.js, React (Vite).

Related guides

Related tools