Introduction
vite-env is a Vite plugin that replaces ad-hoc import.meta.env access with a validated, type-safe environment layer. You define your variables once in an env.ts file using Zod or any Standard Schema-compliant validator (Valibot, ArkType, etc.), and the plugin handles validation, virtual module generation, and type declaration output on every build.
The problem
Vite exposes environment variables as untyped strings on import.meta.env. There is no built-in way to:
- Confirm a required variable is actually set before the app starts
- Prevent server secrets from reaching the client bundle
- Keep TypeScript types in sync with what
.envactually contains
vite-env solves all three.
Defining your environment
Everything starts from a single env.ts file at the root of your project:
import { defineEnv } from '@vite-env/core'
import { z } from 'zod'
export default defineEnv({
server: {
DATABASE_URL: z.url(),
JWT_SECRET: z.string().min(32),
DB_POOL_SIZE: z.coerce.number().int().min(1).max(100).default(10),
REDIS_URL: z.url().optional(),
},
client: {
VITE_API_URL: z.url(),
VITE_APP_NAME: z.string().min(1),
VITE_DEBUG: z.stringbool().default(false),
VITE_LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
VITE_NODE_ENV: z
.enum(['development', 'test', 'production'])
.default('development'),
},
})defineEnv() takes two sections:
server
Keys in server hold secrets and infrastructure values that must never reach the browser — things like DATABASE_URL and JWT_SECRET. They have no naming restriction. These variables are available only via virtual:env/server, which is a server-side import. Any attempt to import that module in client code will fail at bundle time.
client
Keys in client are exposed to both the client bundle and server code. Every key must be prefixed with VITE_ — defineEnv throws at startup if one is not. This mirrors Vite's own convention and makes the exposure boundary explicit.
Client variables are available via virtual:env/client.
What happens at build time
When Vite starts (dev server or production build), the plugin runs through this sequence:
- Loads your
env.tsdefinition - Reads all
.envfiles using Vite's standard env loading rules - Validates every variable against your schema — if anything is missing or invalid, the build fails immediately with a formatted error listing every problem
- Generates
vite-env.d.tsin your project root with TypeScript types (rich inference with Zod,stringfallback with Standard Schema) - Makes
virtual:env/clientandvirtual:env/serveravailable as importable modules with fully typed, validated values
You never access a variable that failed validation. You never drift out of sync with your types.
Ready to add it to a project? See Quick Start.