Vite Env Undefined in Production: Fix It at Build Time
When a Vite environment variable works in development but is undefined after deployment, check the build boundary first. Client code should read import.meta.env.VITE_NAME, the variable must be available to the build, and changing a deployment variable after static assets were built requires another build.
Vite exposes environment variables as strings and statically replaces them during bundling. That is why a browser bundle cannot automatically read a new server process environment after the files have already been generated.
Check the five common boundaries
| Symptom | Likely cause | First fix |
|---|---|---|
process.env.API_URL is undefined in browser code | Node-style access is being used in a Vite client module | Read import.meta.env.VITE_API_URL |
import.meta.env.API_URL is undefined | The variable has no exposed prefix | Rename it to VITE_API_URL only if it is safe to expose |
| It works locally but not in production | The production build did not receive the variable | Set it before vite build and rebuild |
It exists in .env.staging but not in the bundle | The build is using another mode | Pass --mode staging or use the mode that the build actually selects |
| A secret appears in a client bundle | A sensitive value was given a public prefix | Move the secret behind a server or serverless endpoint |
The Vite environment and mode guide is the source of truth for these loading rules. Do not debug all five boundaries by changing the deployment platform at once; verify them in order.
Use import.meta.env and expose only public values
Given this .env file:
VITE_API_URL=https://api.example.comDATABASE_PASSWORD=not-for-the-browserRead the values like this:
const apiUrl = import.meta.env.VITE_API_URL;const databasePassword = import.meta.env.DATABASE_PASSWORD;
console.log(apiUrl); // a stringconsole.log(databasePassword); // undefined in client codeThe VITE_ prefix is an exposure rule, not a security mechanism. Anything exposed under that prefix is bundled into code that a visitor can download. API keys, database passwords, private tokens, and signing secrets do not belong in VITE_* variables.
If your project needs a different public prefix, configure Vite’s envPrefix deliberately and review the result as part of a security check. A broader prefix makes accidental exposure easier.
Load environment values inside vite.config.*
Application code and config evaluation are separate boundaries. Vite resolves vite.config.* before .env* values are automatically loaded into process.env. If the config itself needs a .env value, load it with loadEnv using the current mode:
import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), 'VITE_');
return { define: { __API_URL__: JSON.stringify(env.VITE_API_URL), }, };});This example assumes process.cwd() is the Vite project root. If the config uses a different envDir, pass that directory to loadEnv. If only browser code needs the value, keep using the literal import.meta.env.VITE_API_URL reference instead of moving server-side values into the client bundle.
Match the .env file to the build mode
Vite loads the general files and then the mode-specific files:
.env.env.local.env.[mode].env.[mode].localFor a staging build, make the mode explicit:
pnpm exec vite build --mode stagingThen place the public value in .env.staging or inject it into the build environment:
VITE_API_URL=https://staging-api.example.comThe mode is a build input. If a CI job runs the default production build, .env.staging will not be the file that supplies its values. Existing process environment variables also take precedence, so a stale CI variable can override a correct file.
Treat static deployment variables as build inputs
For a static Vite site, this sequence matters:
environment variables -> vite build -> JavaScript and HTML assets -> CDNIf you change VITE_API_URL in a hosting dashboard after the build has finished, the old bundle remains unchanged until the build runs again. Rebuild and redeploy, then verify the generated site rather than only checking the dashboard setting.
When the value must change at runtime without rebuilding, use a runtime configuration endpoint or server-rendered value instead of pretending a static client bundle has live access to the host environment. Keep the runtime response limited to values that are safe for the browser.
Make missing configuration fail clearly
Do not wait for a production request to reveal a missing URL. Centralize the check:
const apiUrl = import.meta.env.VITE_API_URL;
if (!apiUrl) { throw new Error('VITE_API_URL is missing from this build');}
export const config = { apiUrl };For a more defensive build, add a CI check that prints the variable name and whether it exists, never its secret value. The check should run before the bundler so the failure points to configuration instead of a later network error.
In a CI job that injects VITE_API_URL, fail before the bundler if the variable is absent:
test -n "${VITE_API_URL:-}" || { echo "VITE_API_URL is missing before vite build" exit 1}pnpm exec vite build --mode productionTo verify the boundary without printing a real URL, use a non-secret marker in an isolated build:
VITE_API_URL=https://api.example.test pnpm exec vite build --mode productionrg -n 'api\.example\.test' dist/A match proves that this public marker entered the generated assets. No match means the exact marker was not emitted as a contiguous string; check the selected mode, envDir, and final build directory before inspecting the host dashboard.
If the project is an Astro app, keep the Vite client/server boundary in mind: browser-exposed values and server-only values do not have the same safety requirements. The site’s Vite manualChunks migration guide is a related build-time troubleshooting example, while Astro’s base-path guide covers a different static-build boundary.
FAQ
Q: Why is a Vite variable undefined only after deployment?
A: The production build likely did not receive the variable, the name lacks the VITE_ prefix, or the build used a different mode. Check the final build command and its environment first.
Q: Do I need to restart Vite after editing .env?
A: Restart the dev server, and rebuild for production. Environment values are loaded as part of the Vite process and are replaced into the output during bundling.
Q: Why is an environment variable missing in vite.config.ts?
A: Vite resolves the config before .env* values are automatically loaded into process.env. Use loadEnv(mode, envDir, prefix) in the config callback; use import.meta.env for application code.
Q: Can I put an API secret in VITE_API_KEY?
A: No. A VITE_ value is intended for client exposure. Keep secrets on a server or serverless function and let the browser call a controlled endpoint.
References:
Report a typo or broken link, or suggest a related topic.