Vite exposes environment variables on import.meta.env, but only the ones prefixed with VITE_ ever reach the browser — everything else is stripped so a stray DB_PASSWORD in your .env cannot leak into the bundle. The values are statically replaced at build time, not read at runtime, which is why one of the most-searched Vite questions is "why is my env variable undefined in production?". The mechanism has been stable since Vite 2 (2021), and Vite 8.0 (March 12, 2026) swapped the entire bundler for Rust-based Rolldown without changing a single rule on this page.
TL;DR
- Only
VITE_-prefixed variables appear onimport.meta.envin app code. Unprefixed variables areundefinedby design. - Values are inlined into the JavaScript at build time — anyone can read them in DevTools. Never put a secret in a
VITE_variable. - File priority:
.env.[mode].local>.env.[mode]>.env.local>.env— and a variable already set in the shell beats them all. vite buildreads.env.production,vite(dev) reads.env.development. Mode andNODE_ENVare different concepts.- Inside
vite.config.ts,import.meta.envdoes not exist — use theloadEnv()helper.
Which .env files does Vite load?
Vite uses dotenv under the hood and reads up to four files from your project root (or envDir if you changed it). More specific files win:
| File | Loaded when | Priority |
|---|---|---|
.env.[mode].local | Matching mode only, git-ignored | Highest file |
.env.[mode] | Matching mode only (e.g. .env.production) | High |
.env.local | Always, git-ignored | Medium |
.env | Always | Lowest |
One rule sits above all four files: a variable that already exists in the process environment when Vite starts — exported in your shell, set by CI, baked into a Docker image — is never overwritten by a .env file. The same first-wins behavior trips people up in plain dotenv too. And because files are read once at startup, editing .env requires a dev-server restart — Vite hot-reloads your code, not your environment.
Why is import.meta.env.VITE_MY_VAR undefined?
Five causes account for nearly every report of this. Work down the list:
- Missing the prefix.
API_URL=...in.envis loaded but never exposed. Rename itVITE_API_URL. - Dynamic key access. Replacement is static text substitution:
import.meta.env.VITE_API_URLgets rewritten,import.meta.env[key]does not and evaluates toundefinedin the production bundle. Always use the literal dotted form. - Set after the build. Adding the variable to your hosting dashboard does nothing for a bundle that was already built — the value was inlined (or not) at
vite buildtime. Rebuild and redeploy. - Wrong file for the mode. The value lives in
.env.developmentbut you ranvite build, which loads.env.production. - No server restart. You added the variable while
vitewas running. Restart the dev server.
// .env
VITE_API_URL=https://api.example.com
DB_PASSWORD=hunter2 # loaded, but never exposed to client code
// src/api.ts
console.log(import.meta.env.VITE_API_URL); // "https://api.example.com"
console.log(import.meta.env.DB_PASSWORD); // undefined — no VITE_ prefix
// Static replacement only works on the literal form:
const key = 'VITE_API_URL';
console.log(import.meta.env[key]); // undefined in the built bundleWhat is the difference between mode and NODE_ENV?
Vite deliberately decouples the two. Mode picks which .env.[mode] files load and sets import.meta.env.MODE. NODE_ENV controls import.meta.env.PROD / DEV and what libraries like React strip in production. A staging build is the canonical use case: production-grade output, staging configuration.
# Dev server: mode = development, loads .env.development
vite
# Production build: mode = production, loads .env.production
vite build
# Staging: production-optimized build that loads .env.staging
vite build --mode staging| Command | import.meta.env.MODE | PROD |
|---|---|---|
vite | development | false |
vite build | production | true |
vite build --mode staging | staging | true |
NODE_ENV=development vite build | production | false |
Besides MODE, Vite ships four more built-in constants: BASE_URL (from the base option), PROD, DEV, and SSR.
How do you read env variables inside vite.config.ts?
You cannot use import.meta.env in the config file — the config runs in Node before any .env file is parsed. When a value from .env must influence the config itself (a dev-server port, a plugin toggle), load the files manually with loadEnv():
import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => {
// Third argument '' loads ALL variables, not just VITE_-prefixed ones
const env = loadEnv(mode, process.cwd(), '');
return {
server: {
port: Number(env.DEV_SERVER_PORT) || 5173,
proxy: {
'/api': { target: env.API_PROXY_TARGET, changeOrigin: true },
},
},
};
});Values returned by loadEnv stay in the config — they are not exposed to client code unless they carry the prefix. Everything is a string, exactly like process.env in Node.js, so VITE_FLAG=false is the truthy string "false" until you compare it explicitly.
When should you use define instead of env files?
The define config option creates build-time constants that do not come from the environment at all — version numbers, build timestamps, git SHAs. Values must be JSON-serializable strings, so wrap them in JSON.stringify; forgetting that is the classic "define replaced my string with an identifier" bug.
// vite.config.ts
import { defineConfig } from 'vite';
import pkg from './package.json';
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify(pkg.version),
__BUILD_TIME__: JSON.stringify(new Date().toISOString()),
},
});
// anywhere in app code
console.log(`v${__APP_VERSION__} built ${__BUILD_TIME__}`);Rule of thumb: define for values derived from the build itself, .env files for values that differ per environment. Both end up statically inlined.
Are VITE_ variables exposed to anyone who visits the site?
Yes — every VITE_ value is plain text inside your shipped JavaScript. Open DevTools, search the bundle, and it is there. The official docs are blunt about it: VITE_* variables should not contain sensitive information. The same applies to NEXT_PUBLIC_ in Next.js and REACT_APP_ in CRA — the prefix is a publication marker, not a protection. Anything secret (API keys with billing attached, database URLs, signing keys) belongs on a backend that proxies the request; the env vars security guide covers the threat model.
Never set envPrefix: ''
envPrefix lets you customize which prefix gets exposed (e.g. ['VITE_', 'PUBLIC_']). Setting it to an empty string would expose your entire environment — every CI token and shell secret — to the client bundle. Vite refuses to start with that config and throws, which tells you how bad an idea it is.
How do you type import.meta.env in TypeScript?
Custom variables are typed as any until you augment ImportMetaEnv. Scaffolded Vite projects ship a src/vite-env.d.ts for exactly this:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_SENTRY_DSN: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}One sharp edge: the augmentation silently stops working if the file contains a top-level import statement, because that turns it from an ambient declaration into a module. Keep vite-env.d.ts import-free.
Vite can also substitute values into index.html with the %VITE_NAME% syntax — useful for analytics IDs in a script tag. Unknown names are left as-is rather than becoming undefined.
When should you not use Vite env variables?
- Secrets of any kind — the values ship to every visitor. Proxy through a backend or serverless function instead.
- Runtime configuration in a single Docker image — values are frozen at
vite build. If you need "build once, deploy to N environments", inject config at container start (an entrypoint script writingwindow.__CONFIG__or a generatedconfig.jsonfetched at boot) rather than rebuilding per environment. - Server-side values in SSR frameworks — in TanStack Start, Nuxt, or SvelteKit on Vite, server code can read
process.envdirectly at runtime. ReserveVITE_for values the browser genuinely needs.
Frequently Asked Questions
Why is my Vite env variable undefined in production?
Usually one of: the variable lacks the VITE_ prefix, it was added to the hosting dashboard after the build (values are inlined at build time — rebuild), it lives in .env.development while vite build reads .env.production, or it is accessed dynamically as import.meta.env[key], which static replacement cannot rewrite.
How do I expose an env variable to client code in Vite?
Prefix it with VITE_ in any .env file (e.g. VITE_API_URL=https://api.example.com) and read it as import.meta.env.VITE_API_URL using the literal dotted form. Restart the dev server after editing .env files — they are read once at startup.
What is the difference between mode and NODE_ENV in Vite?
Mode selects which .env.[mode] files load and sets import.meta.env.MODE; NODE_ENV controls import.meta.env.PROD and DEV. vite build --mode staging produces a production-optimized bundle (PROD=true) that loads .env.staging — the standard staging-deploy pattern.
Can I use import.meta.env inside vite.config.ts?
No. The config executes in Node before .env files are parsed, so import.meta.env is not populated there. Use the loadEnv(mode, process.cwd(), prefix) helper exported from vite; passing an empty string as the prefix loads all variables, not just VITE_-prefixed ones.
Are VITE_ environment variables secure?
No. Every VITE_ value is inlined as plain text into the shipped JavaScript bundle and readable by anyone in DevTools. The prefix marks a variable as public, it does not protect it. Keep API keys and secrets on a server and proxy requests.
Does Vite 8 change how env variables work?
No. Vite 8.0 (March 12, 2026) replaced esbuild and Rollup with the Rust-based Rolldown bundler, but the env system — VITE_ prefix, .env file priority, static replacement, loadEnv — is unchanged from earlier majors.
References
- Vite docs — Env Variables and Modes — the canonical reference for file priority, modes, and built-in constants
- Vite docs — shared options (
envDir,envPrefix,define) - Vite 8.0 announcement — the Rolldown release; confirms env handling is unchanged
- Vite migration guide — upgrade notes between majors
Validate your .env files with the env validator, or read the .env syntax rules guide for quoting and multiline edge cases.