Build-Time Env Validation with Vite

2026-06-21·Ctrotech·
guidevite

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

FeatureVite import.meta.envCtroEnv plugin
Type validationNone (always string)TypeScript types + runtime
Default valuesManual ?? "default".default() in schema
Boolean coercionManual parsingboolean() validator
Build-time checkOnly if referencedAll vars validated
Secret maskingNot available.secret() support