Vite Environment Variables: Designing a Safe Client‑Side Config
Learn how to safely expose only non‑secret configuration to the browser using Vite’s VITE_-prefixed environment variables, understand the trust boundary, and verify that no secrets leak into the built bundle.
09 Jun 2026, 15:05 UTC

Problem
A single‑page application built with Vite needs per‑environment values such as API base URLs, feature flags, or public keys. Committing secrets to the repository or rebuilding a separate artifact for each tenant is undesirable, yet the values must be available to client‑side code at runtime.
Requirements
- Expose only non‑secret configuration to the browser.
- Keep the build process simple – no extra plugins or custom scripts.
- Allow different values per mode (development, staging, production) via .env files.
- Ensure the values are frozen into the built bundle so they cannot be changed without a rebuild.
Smallest Suitable Design
- Create .env files in the project root:
.env(shared),.env.development,.env.production, etc. - Prefix every variable that should be visible to client code with
VITE_. Example: - In application source, read values exclusively through
import.meta.env: - All other variables (without the
VITE_prefix) remain server‑side only and are never inlined into the client bundle.
# .env.production
VITE_API_URL=https://api.example.com
VITE_FEATURE_FLAG_NEW_UI=true
# SECRET_KEY must NOT have VITE_ prefix
SECRET_KEY=super‑secret
// src/api.js
export const apiBase = import.meta.env.VITE_API_URL;
Trust / Data Boundary
The trust boundary is defined by the VITE_ prefix, not by the file name. During both dev server execution and production build, Vite performs static textual replacement of import.meta.env.VITE_* references. Consequently:
- Every
VITE_-prefixed value is embedded directly into the shipped JavaScript and is readable by anyone who can download the bundle. - Files such as
.env.localor.env.production.localdo not provide secrecy for prefixed values; they only affect which values are loaded. - Secrets must never receive the
VITE_prefix, otherwise they will be exposed in the client bundle.
Operational Checks
After a production build, verify that no secret has leaked and that the expected values are present:
- Run the build (requires read access to the project and write access to the output directory):
- Search the emitted assets for each
VITE_key and for any known secret patterns: - In CI, add a lint step that fails when a
VITE_-prefixed variable matches a secret‑like regex (KEY|TOKEN|SECRET|PASSWORD). - Smoke‑test the built artifact (not the dev server) by loading the page in a browser and confirming that
import.meta.env.VITE_API_URLresolves to the expected value.
# Run in the project root
npm run build # or: vite build
# Example: check for accidental exposure of a secret named SECRET_KEY
grep -r "SECRET_KEY" dist/
# Example: verify that the API URL is inlined
grep -r "VITE_API_URL" dist/ | head -n 5
Failure Modes
- Secret leakage: If a secret is mistakenly given the
VITE_prefix, it will be visible in the bundle. The grep check above will catch it if the secret is known. - Stale values in dev: Editing a
.envfile while the dev server is running may not trigger a reload; you must restart the server to see the change. - Source‑map exposure: Source maps can contain the original variable names and surrounding code, making it easier to locate inlined values. Disable source maps for production if this is a concern.
- Dynamic access: Using
import.meta.env[someKey]does not reliably resolve customVITE_variables because the replacement is static; avoid this pattern.
When the Design Must Change
If a single deployed artifact must serve multiple environments with different values (e.g., a containerized service that receives its configuration at runtime, or a multi‑tenant CDN edge), build‑time inlining no longer fits. In those cases consider:
- Runtime configuration endpoint (
/config.json) fetched by the app on load. - An inline script injected by the server that sets
window.__CONFIG__. - Server‑side rendering that injects the values into the HTML.
Switching to any of these approaches moves the trust boundary from the client bundle to the server, requiring a review of authentication and caching.
Limitations and Practical Verification
This note describes the behavior of Vite as of the 5.x series; exact file precedence and dev‑server restart semantics can differ between major versions. Always verify against the documentation for the version you are using.
To confirm that your setup respects the trust boundary:
- Pin a Vite version in
package.json(e.g., "vite": "^5.2.0"). - Run a production build.
- Inspect the generated bundle for any values that should remain secret.
- If a secret appears, remove the
VITE_prefix, rebuild, and re‑inspect.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.