TL;DR
- Both
NEXT_PUBLIC_andVITE_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_KEYis 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
| Aspect | Next.js | Vite |
|---|---|---|
| Prefix | NEXT_PUBLIC_ | VITE_ |
| Access in client code | process.env.NEXT_PUBLIC_X | import.meta.env.VITE_X |
| Access on server | process.env.X (all vars) | process.env.X (via Node) |
| Config file | .env.local, .env.production | .env.local, .env.production |
| Typed by default | No (string only) | No (string only) |
| Build-time or runtime? | Build-time (inlined) | Build-time (inlined) |
| Requires rebuild to change | Yes | Yes |
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 // booleanNext.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.envbefore deploying. - Comments in
.envwork 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.localin test mode by default. Vite respects.env.testwhenNODE_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).