Fix environment variables undefined in the browser (Vite / Next.js prefix rules)
The problem
API_URL read fine in server code and came back undefined in the browser bundle. Two separate causes bit in the same afternoon: Vite only inlines variables prefixed VITE_ (and exposes them via import.meta.env, not process.env), Next.js only inlines NEXT_PUBLIC_ ones — and both inline at build/server-START time, so a var added to .env after the process started stays missing until restart.
What didn't work
- Restarting only the frontend dev server while the other half of the stack kept the old env — one process sees the var, the other doesn't.
- Dropping a secret into a
NEXT_PUBLIC_/VITE_variable — it is literally text-substituted into the shipped JS bundle; anyone can read it in DevTools. process.env.VITE_API_URL = env.VITE_API_URLin a config file — runtime assignment does nothing in the browser; replacement happens at build time.
The fix
# .env.local (never commit real keys — .env.local is gitignored by convention)
VITE_API_URL=https://api.example.com # client-safe: inlined at dev-server START
SECRET_DB_URL=postgres://... # server-only: read at RUN time on the server
// vite.config.ts — fail fast on typos and missing vars instead of a silent undefined
import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), 'VITE_');
if (!env.VITE_API_URL) {
throw new Error('VITE_API_URL missing — restart the dev server after editing .env');
}
return { define: { 'process.env.VITE_API_URL': JSON.stringify(env.VITE_API_URL) } };
});
For Next.js: prefix client vars NEXT_PUBLIC_API_URL, and remember a newly added one requires a full rebuild in production, not just a redeploy of the server.
Why it works
Both bundlers inline client env as literal strings at their own build/start moment; the fix is matching the bundler's prefix rule, restarting the process that inlines it, and converting the silent undefined into a boot-time error that names the missing variable.