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