Build-Time Env Validation with Vite
Build-Time Env Validation with Vite
Vite builds your app and bundles env vars at build time. CtroEnv's Vite plugin validates everything before the build completes — no broken deployments from missing env vars.
The Plugin
ctroenvPlugin() runs validation in the buildStart hook. If validation fails, the build stops:
// vite.config.ts
import { defineConfig } from "vite"
import { ctroenvPlugin } from "@ctroenv/vite"
export default defineConfig({
plugins: [
ctroenvPlugin({ schema: "./src/env.ts" }),
],
})
Schema Options
The plugin accepts a schema as either a file path or an inline definition:
File path
ctroenvPlugin({ schema: "./src/env.ts" })
The plugin imports the module and looks for an export named schema, then env, then default. Your schema file:
// src/env.ts
import { string, number } from "@ctroenv/core"
export const schema = {
DATABASE_URL: string().url(),
PORT: number().port().default(3000),
}
Inline definition
ctroenvPlugin({
schema: {
DATABASE_URL: string().url(),
PORT: number().port().default(3000),
},
})
Fail on Error
By default, validation errors stop the build with this.error(). Set failOnError: false to warn instead:
ctroenvPlugin({
schema: "./src/env.ts",
failOnError: false, // warn instead of failing
})
When failOnError is false, the plugin calls this.warn(). The build continues but the warning appears in the terminal output:
✓ CtroEnv: All environment variables valid
Or if validation fails:
✗ CtroEnv: Missing required environment variable: DATABASE_URL
Secret Masking
Use maskWith for a custom mask string:
ctroenvPlugin({
schema: "./src/env.ts",
maskWith: "***",
})
The Source Adapter
viteSource() reads from import.meta.env first, then falls back to process.env. This means the plugin works during both development (vite dev) and production builds (vite build):
import { defineEnv } from "@ctroenv/core"
import { viteSource } from "@ctroenv/vite"
const env = defineEnv(schema, { source: viteSource() })
CI Integration
Add the validation to your CI pipeline:
# .github/workflows/build.yml
- run: npm run build
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
The build fails if any required env var is missing, even if the TypeScript compiles fine.
Migration from Vite's import.meta.env
Replace scattered import.meta.env.VITE_* references with typed env access:
// Before
const apiUrl = import.meta.env.VITE_API_URL
const debug = import.meta.env.VITE_DEBUG === "true"
// After
import { env } from "./env"
env.NEXT_PUBLIC_API_URL // string — validated
env.VITE_DEBUG // boolean — parsed and typed
Comparison with Vite's Built-in
| Feature | Vite import.meta.env | CtroEnv plugin |
|---|---|---|
| Type validation | None (always string) | TypeScript types + runtime |
| Default values | Manual ?? "default" | .default() in schema |
| Boolean coercion | Manual parsing | boolean() validator |
| Build-time check | Only if referenced | All vars validated |
| Secret masking | Not available | .secret() support |