Framework-Specific Env Patterns
Your schema is portable. But each runtime loads environment variables differently. CtroEnv adapters bridge the gap — same validation logic, different data sources. Node.js: process.env + .env Files The @ctroenv/node adapter loads .env files and wraps process.env : import { defineEnv , string , number } from " @ctroenv/core " import { loadEnv } from " @ctroenv/node " const env = defineEnv ( schema , { source : loadEnv () }) loadEnv() resolves files in order: .env — shared defaults .env.{NODE_ENV} — environment-specific ( .env.development , .env.production ) .env.local — local overrides (gitignored) Later files override earlier ones. process.env takes precedence unless override: true . Monorepo Root loadEnv ({ path : " ../.. " }) // look up two directories for root .env Native Node 22+ Node 22 has built-in process.loadEnvFile() . Use native: true to delegate: loadEnv ({ native : true }) // uses process.loadEnvFile() if available Falls back to the custom parser on older Node versions. System Fallback By default, only file values are returned. With system: true , missing keys fall through to process.env : loadEnv ({ system : true }) Standalone Parser Use parseEnvFile() directly for custom file loading: import { parseEnvFile } from " @ctroenv/node " const content = readFileSync ( " .env.custom " , " utf-8 " ) const vars = parseEnvFile ( content ) Handles quotes, multiline values (backslash continuation), interpolation ( ${VAR} ), comments, and export prefix. Vite: Build-Time Validation The @ctroenv/vite plugin validates during the build: // vite.config.ts import { ctroenvPlugin } from " @ctroenv/vite " export default defineConfig ({ plugins : [ ctroenvPlugin ({ schema : " ./src/env.ts " }), ], }) If DATABASE_URL is missing, the build fails — no broken artifacts shipped. Schema Options Pass a file path or inline definition: // File path — imports the module, looks for `schema` export ctroenvPlugin ({ schema : " ./src/env.ts " }) // Inline definition ctroenvPlugin ({ schem