Your Vite Environment Variables Are Public: What To Do

Every VITE_ value is baked into your bundle as plain text. How your Vite environment variables work, why they come out undefined, and where secrets belong.
The OxyPages Team · · 8 min read
You put an API key in .env, named it VITE_API_KEY because that's the only way it would show up in your code, and the app works. Then someone mentions that the key is sitting in your JavaScript bundle for anyone to read.
They're right. Every value in your Vite environment variables that starts with VITE_ is copied into the built JavaScript as plain text. Not encrypted, not hidden, just a string in a file every visitor downloads.
I checked this on a fresh Vite 8.3.6 build in November 2026. I put two values in .env, one with the prefix and one without, and searched the output. The VITE_ value appeared word for word in dist/assets/index-*.js. The other one came out as void 0, which is JavaScript for undefined. A rerun on Vite 8.3.8 on 13 November produced the same line.
That one test explains both problems people search for: keys leaking, and variables mysteriously being undefined. Below is how the prefix works, why "runtime" variables don't exist in a static build, the six reasons a value comes out undefined, and where a real secret should live instead. If the app isn't deployed yet, how to deploy a Vite app from its dist folder covers the build side first.
TL;DR: Vite replaces import.meta.env.VITE_* with the literal value at build time, so anything with the VITE_ prefix ends up readable in the browser bundle. Variables without the prefix are never exposed and come out undefined in your code. There are no runtime variables in a static build: change a value and you must rebuild. Public identifiers like a Stripe publishable key or a restricted Maps key are fine in VITE_. Secret keys need a server or serverless function, whichever host you use.
What Happens To Your Vite Environment Variables At Build Time?
Vite reads your .env files when it starts, then swaps every import.meta.env.VITE_SOMETHING in your code for the actual value. Vite's guide to env variables and modes says these constants are "statically replaced at build time", and that variables prefixed with VITE_ "will be exposed in client-side source code after Vite bundling".
Here's what my test looked like. The .env file:
VITE_MAPS_KEY=demo-public-key-123
STRIPE_SECRET=sk_test_should_not_ship
The code:
console.log(import.meta.env.VITE_MAPS_KEY, import.meta.env.STRIPE_SECRET, import.meta.env.MODE)
And what that line became in the built bundle:
console.log(`demo-public-key-123`,void 0,`production`)
No lookup happens when the page runs. The value is part of the code, the same as if you'd typed it in. That's why the prefix exists: it's Vite's way of making you opt in, one variable at a time, to publishing a value. You can rename the prefix with envPrefix, but never to an empty string. Vite's envPrefix security note says that would expose every variable, and Vite throws an error if you try.
Vite also gives you a few built-ins without any prefix: import.meta.env.MODE, BASE_URL, PROD, DEV and SSR. BASE_URL matters if the app lives in a subfolder, and the guide to the Vite base path covers it.
Which Files Does Vite Load, And In What Order?
Vite loads up to four .env files, and the more specific one wins:
| File | Loaded when | Commit it? |
|---|---|---|
.env | Always | Yes, if it holds only safe defaults |
.env.local | Always | No. Git-ignore every *.local file |
.env.[mode], like .env.production | Only in that mode | Yes, if it holds no secrets |
.env.[mode].local | Only in that mode | No |
The dev server runs in development mode and vite build runs in production mode, unless you pass --mode. Mode files beat the generic ones, and a variable already set in your shell or your host's build settings beats every file. Vite's docs say existing environment variables "will not be overwritten by .env files."
That last rule matters on a host that builds for you. A value set in its dashboard overrides whatever is in your committed .env.production.
Why Is My Vite Variable Undefined?
There are six common causes, and the first two cover most cases:
- The name has no
VITE_prefix. Vite only exposes prefixed names, soAPI_URLis alwaysundefinedin browser code. Rename itVITE_API_URL. - You didn't restart the dev server. Vite's docs say
.envfiles load "at the start of Vite", so edits do nothing until you restart. - You used
process.env. That's Node's object and it doesn't exist in the browser. In Vite code it'simport.meta.env. - The file is in the wrong folder.
.envbelongs in the project root, next tovite.config.js, unless you've changedenvDir. - The host built without it. If your host runs the build, the variable has to be set in its build settings before the build starts. Adding it afterwards changes nothing until the next build.
- The mode is wrong. A value that only lives in
.env.developmentwon't exist innpm run build, which uses production mode.
Number 5 catches people who expect a host's environment settings to work like a server's. They don't, and the next section explains why.
There Are No Runtime Variables In A Static Build

A static build has no runtime environment to read from. The JavaScript runs in your visitor's browser, which has no idea what's in your .env or your host's settings. Every value was decided when npm run build ran.
So if you came here looking for runtime values, the honest answer is that you have two options:
- Rebuild per environment. Staging and production each get their own build with their own values. This is what every host that builds from Git does, usually with separate Production and Preview values.
- Fetch a config file at startup. Put a
config.jsoninpublic, deploy a different one per environment, and have the app fetch it before rendering. One build, many configs. Everything in that file is just as public as aVITE_value, though.
Neither option makes a value secret. If the browser can use it, the browser's user can read it.
What's Safe In A VITE_ Variable And What Isn't
A value is safe in your Vite environment variables if it's designed to be public. Some keys are made for the browser and protected some other way:
| Value | Safe in VITE_? | Why |
|---|---|---|
Stripe publishable key (pk_...) | Yes | Built for client use. The secret key (sk_...) never is |
| Supabase anon or publishable key | Yes, with row-level security on | The database rules protect the data, not the key |
| Google Maps key restricted to your domain | Yes | The referrer restriction stops reuse elsewhere |
| Your API's base URL | Yes | It's visible in the network tab anyway |
| An OpenAI or Anthropic API key | No | Anyone can spend your credit with it |
| A database password or service-role key | No | Full access to your data |
| A webhook signing secret | No | It's only useful if nobody else knows it |
The test I use: if a stranger copied this value out of your bundle, could they do anything you'd pay for or regret? If yes, it doesn't belong in front-end code at all.
Where A Secret Key Should Live Instead

A secret belongs in code that runs on a server, where visitors only ever see the result. Vite's docs suggest the same thing: a backend, or serverless and edge functions.
In practice that means a small function that holds the key and calls the API for you. Your front end calls /api/summarise, the function adds the key, and the browser never sees it. Vercel Functions, Netlify Functions and Cloudflare Workers all do this well.
A pure static host can't, and that includes OxyPages, which serves your built files and runs no server code. If your app needs a secret at request time, pair the static front end with one of those function platforms, or host the whole thing there. The rundown of React hosting options compares them.
Hosts that build from your repository usually split variables into kinds. On OxyPages, for example, a project built from Git (in beta as of November 2026) has Secret, Plain and Build variables, with separate Production and Preview values. For now everything there is used at build time only, and it refuses to save a VITE_ name as a Secret, because the framework would copy it into the bundle anyway. That rule is worth copying wherever you build: a secret with a public prefix is a contradiction.
Walkthrough: Moving A Leaked Key Out Of The Bundle
Say an AI assistant built you a page that summarises text, and it put your OpenAI key in VITE_OPENAI_KEY. Here's the fix:
- Revoke the key now at the provider and create a new one. Once a key has shipped in a bundle, assume it's been copied. Deleting it from the code doesn't un-publish old builds or cached copies.
- Search the build. Run
npm run build, then searchdistfor the first few characters of the old key to confirm where it was. - Rename the variable without the prefix, like
OPENAI_KEY, and set it only on the function platform, never in.envfiles you commit. - Write a small function that reads
OPENAI_KEY, calls the API and returns the summary. - Change the front end to call your function's URL instead of the provider directly.
- Rebuild and search again. The new key's first characters shouldn't appear anywhere in
dist.
If the code came from an AI tool and you're nervous about changing it, how to edit AI-generated code safely covers the habits that keep a working copy.
What To Do Next
Open your .env files and sort every variable into public or secret. Rename anything secret so it has no VITE_ prefix and move it behind a function. Then build and search dist for each secret's first few characters. If nothing turns up, you're clean.
If you're comparing hosts that build from Git with Production and Preview values, see what each OxyPages plan includes alongside the function platforms above. A static Vite build with no secrets runs fine on the Free plan for a personal project: start a free account, no card needed.
FAQ
Can I Change A Vite Variable Without Rebuilding?
No. Values are written into the code during npm run build, so a new value needs a new build. If you need one build to work in several environments, fetch a config file at startup instead, keeping in mind that it's public too.
Why Does import.meta.env Work In Dev But Not In Production?
Usually because the value lives only in .env.development or .env.local on your machine, and the production build ran somewhere that doesn't have it. Set the variable in your host's build settings, then trigger a new build.
Can I Change The VITE_ Prefix?
Yes, with the envPrefix option in vite.config.js. You can't set it to an empty string, though. Vite's docs say an empty prefix would expose every variable, and it throws an error if you try.
Are Your Vite Environment Variables Secure?
Only the ones without the VITE_ prefix, and only because they never reach the browser. Anything with the prefix is inlined into the JavaScript as plain text. Treat every VITE_ value as public.