compareupdated 2026-04-19

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.

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