Environments and Configuration
How import.meta.env, the VITE_ prefix and Vite modes work, and why every value you expose to the bundle is public.
import.meta.env.VITE_API_BASE_URLThe API host differs between your machine, staging and production. Hard-coding it means three builds that differ in source. Vite reads .env files instead and substitutes the values at build time.
import.meta.env
Vite defines five properties on every build: MODE (the mode as a string), BASE_URL (from the base config option), PROD, DEV and SSR. DEV is always the opposite of PROD, so import.meta.env.DEV and !import.meta.env.PROD are the same expression written two ways.
The VITE_ prefix
Your own variables reach client code only if they start with VITE_. That prefix is a gate, not a convention: without it, a key in .env stays on the build machine. The gate exists because the alternative is shipping your whole shell environment to the browser. Change the prefix with the envPrefix option if you must.
Files and modes
Vite always loads .env and .env.local, plus the mode-specific .env.[mode] and .env.[mode].local. Later entries win, and real process environment variables beat all of them. vite dev runs in development mode and vite build in production; vite build --mode staging builds in staging mode and reads .env.staging.
Everything here is public
A VITE_ value is written into the JavaScript the browser downloads. Anyone can read it in devtools. Put hosts, feature switches and public keys there; never an API secret. The example renders a small build badge, shown in every mode except production.
Example
<script setup lang="ts">
import { computed } from 'vue'
const mode = import.meta.env.MODE
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL
const commit = import.meta.env.VITE_COMMIT_SHA ?? 'local'
const visible = computed(() => mode !== 'production')
</script>
<template>
<aside v-if="visible" aria-label="Build information">
<strong>{{ mode }}</strong>
<code>{{ apiBaseUrl }}</code>
<code>{{ commit }}</code>
</aside>
</template>
<style scoped>
aside {
display: flex;
gap: 0.75rem;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
</style>When to use it
- Pointing a staging build at a staging API without touching application source.
- Turning a half-finished feature on for staging and off for production.
- Showing the deployed commit in a footer so a bug report names a build.
More examples
Two env files, and what each is for
The first file is committed and holds defaults every developer shares. The second is read only when the build runs with --mode staging, and overrides the keys it repeats. Add a .env.local, which the generated .gitignore already excludes, for values that are yours alone.
# .env
VITE_API_BASE_URL=http://localhost:3000
VITE_PROXY_TARGET=http://localhost:3000
VITE_FEATURE_INVOICE_EXPORT=false
# .env.staging
VITE_API_BASE_URL=https://api.staging.example.com
VITE_FEATURE_INVOICE_EXPORT=trueTyping your own variables
Without this file, import.meta.env.VITE_API_BASE_URL is typed as a loose string index and a typo in the name gives no error. Declaring the keys is only half of it: Vite's own ImportMetaEnv extends Record<string, any>, so the typo stays legal until you also declare strictImportMetaEnv in ViteTypeOptions, which is what makes vue-tsc reject an unknown key. Marking a key optional reminds you to handle the missing case.
// env.d.ts
/// <reference types="vite/client" />
interface ViteTypeOptions {
// Without this line ImportMetaEnv keeps its catch-all index signature,
// so an unknown VITE_ key is still typed any instead of being an error.
strictImportMetaEnv: unknown
}
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string
readonly VITE_FEATURE_INVOICE_EXPORT: string
readonly VITE_COMMIT_SHA?: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
// Output: import.meta.env.VITE_API_BASE_URL is typed string
// Output: import.meta.env.VITE_FEATURE_EXPORT is a compile errorReading env files inside the config
Env variables are not on process.env while vite.config runs, so the config loads them itself with loadEnv(mode, dir, prefix). Use it when a build setting depends on the mode, as the dev proxy target does here. Note that every VITE_ value is a string, so a flag needs an explicit comparison.
// vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), 'VITE_')
return {
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
proxy: {
'/api': {
target: env.VITE_PROXY_TARGET,
changeOrigin: true
}
}
}
}
})
Discussion