@risemaxi/envault
v0.0.3
Published
Build-time validated, typed environment variables for any bundler
Readme
@risemaxi/envault
Build-time validated, typed environment variables for any bundler.
envault validates process.env against a Standard Schema at build time, then exposes the validated values through a virtual module. The schema library never enters your bundle, and a missing or invalid variable fails the build instead of crashing at runtime.
Works with Zod, Valibot, ArkType, Effect Schema, TypeBox — anything implementing the Standard Schema ~standard interface.
envault moves validation to the build step. The schema runs once during bundling; only the validated, typed values reach the output.
Install
npm install -D @risemaxi/envaultQuick start
1. Define a schema
// env.schema.ts
import { z } from "zod";
export default z.object({
API_URL: z.string().url(),
SECRET: z.string().min(1),
PORT: z.coerce.number().default(3000),
});2. Add the plugin
// vite.config.ts
import envault from "@risemaxi/envault/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [envault({ schemaFile: "./env.schema.ts" })],
});3. Import your env
import { env } from "virtual:env";
console.log(env.API_URL); // string
console.log(env.PORT); // numberA ./env.d.ts is generated next to your config so env is fully typed from the schema's output.
Bundlers
Every integration takes the same options. Import the one for your bundler:
import envault from "@risemaxi/envault/vite";
import envault from "@risemaxi/envault/rollup";
import envault from "@risemaxi/envault/rolldown";
import envault from "@risemaxi/envault/webpack";
import envault from "@risemaxi/envault/rspack";
import envault from "@risemaxi/envault/esbuild";
import envault from "@risemaxi/envault/bun";// webpack.config.js
const envault = require("@risemaxi/envault/webpack");
module.exports = {
plugins: [envault({ schemaFile: "./env.schema.ts" })],
};The default export of @risemaxi/envault is the Vite plugin:
import envault from "@risemaxi/envault";Expo / Metro
// metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const { withEnvConfig } = require("@risemaxi/envault/metro");
const config = getDefaultConfig(__dirname);
module.exports = withEnvConfig(config, {
schemaFile: "./env.schema.ts",
});withEnvConfig preserves any existing resolver.resolveRequest (including Expo's defaults) and delegates every other module to it.
Validation runs when the virtual module is first bundled, not when the Metro config is loaded. Tools that merely load your config without bundling — such as EAS Build's fingerprint and config-resolution steps, which run before your project's env vars are injected — therefore don't trip validation on env that isn't present yet.
Metro's resolver is synchronous, so an inline
schemapassed towithEnvConfigmust validate synchronously. UseschemaFilefor asynchronous schemas.
Passing a schema directly
Instead of a file path, pass the schema object inline:
import { z } from "zod";
import envault from "@risemaxi/envault/vite";
const schema = z.object({
API_URL: z.string(),
PORT: z.coerce.number().default(3000),
});
export default {
plugins: [envault({ schema })],
};With an inline schema no .d.ts is generated (there is no file path to reference). Type virtual:env yourself, or use schemaFile to get types for free.
How it works
- For a
schemaFile, envault loads the schema in a child process with jiti, so.ts/.jsschemas and their imports work the same in any project — CommonJS or ESM, any Node ≥ 18 — and validation runs in the child. The schema library stays out of the build process. - For an inline
schema, validation runs directly in the build. - If validation fails, the build fails, listing every missing/invalid variable.
- On success the virtual module is generated as
export const env = { ... }with the validated values inlined. Keys are JSON-quoted, so even names likeMY-VARare reachable viaenv["MY-VAR"]. - A
.d.tsis written soenvis typed directly from the schema's output.
No schema-library code or validation logic ends up in the final bundle.
Where env values come from
envault validates the environment your bundler/runtime provides, using each one's native env loading:
- Vite loads
.envfiles intoimport.meta.env, notprocess.env, so envault reads them through Vite's own loader (honoring yourmodeandenvDir)..env,.env.local, and.env.[mode]work with no extra setup. - Bun and Expo / Metro load
.envfiles intoprocess.env, which envault reads directly. - Rollup, Rolldown, webpack, Rspack, and esbuild have no built-in
.envloading. Populateprocess.envyourself — real environment variables (CI, shell) or a loader likedotenv(import "dotenv/config") before the build.
Real process.env values always take precedence over values from .env files.
Options
| Option | Type | Default | Description |
| --------------- | ------------------ | --------------- | ------------------------------------------------------------------- |
| schema | StandardSchemaV1 | — | A Standard Schema object. Mutually exclusive with schemaFile. |
| schemaFile | string | — | Path (from project root) to a module that default-exports a schema. |
| virtualModule | string | "virtual:env" | The virtual module specifier to import from. |
| dtsFile | string | "./env.d.ts" | Where to write the generated .d.ts. |
schema and schemaFile are mutually exclusive; provide exactly one.
Supported schema libraries
Any library implementing Standard Schema v1:
Exported utilities
import {
envault, // Vite plugin factory (also the default export)
vite,
rollup,
rolldown,
webpack,
rspack,
esbuild,
bun, // per-bundler factories
validateEnv, // (schema, env) => Promise<ValidatedEnv> — async, in-process
validateEnvSync, // (schema, env) => ValidatedEnv — sync, throws on async schemas
loadSchemaEnv, // (schemaFile, env) => ValidatedEnv — load + validate in a child process
clearSchemaCache, // () => void — drop the loadSchemaEnv cache
generateModule, // (env) => string — the virtual module source
generateDts, // (schemaImport, virtualModule?) => string — the .d.ts source
} from "@risemaxi/envault";@risemaxi/envault/metro additionally exports withEnvConfig.
