Vite VITE_ Variables Expose Secrets: Keep Private Values Out of the Bundle
Treat every VITE_* variable as public. Vite replaces exposed variables into the client bundle at build time, so visitors can download the value from JavaScript or inspect it in browser developer tools. API secrets, database passwords, signing keys, and private tokens must stay on a server, serverless function, or edge endpoint instead.
The fix is not to hide the .env file more carefully. Decide which values are safe for a browser, expose only those with the intended prefix, move private operations behind an authenticated server boundary, and rebuild after changing the configuration.
Separate public configuration from private secrets
A safe .env file makes the boundary visible:
# Safe only if the URL is intended for every browser visitorVITE_API_ORIGIN=https://api.example.com
# Server-only values: do not add the VITE_ prefixPAYMENTS_SECRET=replace-me-on-the-serverClient code can read the public value through import.meta.env:
const apiOrigin = import.meta.env.VITE_API_ORIGIN;
fetch(`${apiOrigin}/public-config`);This is not a permission system. The VITE_ prefix tells Vite that a value may be exposed to client-side code; it does not encrypt or protect the value. A value without the prefix is not automatically usable by a browser, but a server-side process can read it through its own runtime configuration.
The Vite environment and mode guide explicitly warns that VITE_* values should not contain sensitive information. Treat a public API key as public too: its presence in a bundle may be expected, but its permissions and server-side restrictions still need to be correct.
Find an exposed value in dist
If you suspect a secret was bundled, inspect the generated output without printing the secret into CI logs. Search for a distinctive non-sensitive fragment or the variable name first:
pnpm exec vite buildrg -n --hidden 'PAYMENTS_SECRET|DATABASE_URL|PRIVATE_TOKEN' distSearching for the variable name can miss a value that Vite replaced directly. For an incident review, download the exact deployed JavaScript and search it locally for a short, non-sensitive marker. Never paste a live token into a command copied to a shared build log.
Also inspect source maps if they are published. A production source map can reveal source code and string literals even when the main bundle is minified. Removing the prefix from a later build does not remove a token from an already deployed asset or from a previous build artifact.
If a real secret was exposed, rotate or revoke it before cleaning up the code. Then remove it from client configuration, rebuild from a clean source of truth, purge the old deployment or cache if your host requires it, and check the deployed assets again.
Use a server or edge proxy for private API calls
The browser should call an endpoint that you control; that endpoint can attach the private credential when it calls the upstream service:
browser -> /api/report -> server or edge function -> private API reads PAYMENTS_SECRETThe client needs only a public route or origin:
const response = await fetch('/api/report', { headers: { 'content-type': 'application/json' }, method: 'POST', body: JSON.stringify({ range: 'week' }),});The server-side handler should validate the user, authorization, input, and upstream response. Do not turn the proxy into a blind URL forwarder; otherwise moving the token to the server only hides the credential while leaving the upstream capability unrestricted.
If a value genuinely must change at runtime for every browser, return only that public value from a runtime configuration endpoint or server-rendered page. A static Vite bundle cannot read a new host environment variable after vite build has finished. The Vite production undefined guide covers that separate build-time loading problem.
Review envPrefix before changing it
Vite exposes VITE_ by default. A project can configure another prefix in vite.config.ts, but the setting should narrow the contract rather than expose more of the process environment:
import { defineConfig } from 'vite';
export default defineConfig({ envPrefix: ['PUBLIC_'],});Now only the selected public prefix is intended for client exposure. Keep server-only names such as DATABASE_URL and PAYMENTS_SECRET outside that prefix.
Do not set envPrefix to an empty string or a broad value to “make every variable available.” The Vite shared options reference warns that this can expose sensitive environment variables. Review envPrefix as part of a security change, not as a shortcut for fixing an undefined value.
Check mode and build timing
Vite loads .env, .env.local, and mode-specific files such as .env.production or .env.staging. The selected mode and existing process environment determine which value reaches the build:
pnpm exec vite build --mode stagingFor a static deployment, the important sequence is:
build environment -> vite build -> HTML and JavaScript assets -> CDNChanging VITE_API_ORIGIN in a hosting dashboard after the asset build does not rewrite the already generated JavaScript. Rebuild and redeploy, then verify the final asset. If you need live private configuration, use a server or edge runtime instead of adding another public prefix.
Before merging, review three things:
- Does the variable need to be known by the browser, or only by a server?
- Does the name match the intended public prefix and selected mode?
- Does the generated
distcontain only values that are safe for every visitor?
The rule to remember is that VITE_ means “ship this to the browser.” If the value would be a security incident when copied into a public gist, it does not belong in a Vite client environment variable.
FAQ
Is a VITE_API_KEY secret?
No. The VITE_ prefix marks the value for client exposure, and Vite replaces it into browser assets during the build. Use it only for a value that is safe for every visitor to see.
Why does removing VITE_ make the value undefined?
Vite intentionally filters client environment variables by prefix. Removing the prefix protects the browser boundary, but the private value must then be read by a server-side process rather than by client code.
Can I hide a Vite secret with a custom envPrefix?
No. A custom prefix changes which names are exposed; it does not make an exposed value private. Use a narrow public prefix and keep secrets in server or edge runtime configuration.
How do I change a Vite environment value without rebuilding?
You cannot change a value already replaced into a static bundle without generating new assets. Use a runtime endpoint or server-rendered configuration for safe public values, and keep private values on the server.
References:
Report a typo or broken link, or suggest a related topic.