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() })
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
Native Node 22+
Node 22 has built-in process.loadEnvFile(). Use native: true to delegate:
loadEnv({ native: true }) // uses process.loadEnvFile() if available
Enter fullscreen mode Exit fullscreen mode
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 })
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode
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" }),
],
})
Enter fullscreen mode Exit fullscreen mode
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({
schema: {
DATABASE_URL: string().url(),
PORT: number().port().default(3000),
},
})
Enter fullscreen mode Exit fullscreen mode
Fail on Error
ctroenvPlugin({ schema: "./src/env.ts", failOnError: false })
// warns instead of failing — useful for optional env vars
Enter fullscreen mode Exit fullscreen mode
viteSource()
Use with defineEnv() directly in Vite code:
import { defineEnv } from "@ctroenv/core"
import { viteSource } from "@ctroenv/vite"
const env = defineEnv(schema, { source: viteSource() })
Enter fullscreen mode Exit fullscreen mode
viteSource() reads from import.meta.env first, then falls back to process.env.
Next.js: Server/Client Split
Next.js bundles code for the browser. Server-only env vars must never reach the client bundle. The @ctroenv/nextjs adapter enforces this at runtime:
import { string, type ClientServerSchema } from "@ctroenv/core"
import { defineEnv } from "@ctroenv/nextjs"
const schema = {
server: {
DATABASE_URL: string().url(),
JWT_SECRET: string().min(32).secret(),
},
client: {
NEXT_PUBLIC_API_URL: string().url(),
},
} satisfies ClientServerSchema
const env = defineEnv(schema)
Enter fullscreen mode Exit fullscreen mode
Server components access everything. Client components can only access NEXT_PUBLIC_ variables — accessing a server var throws:
Server-only environment variable "DATABASE_URL" is not accessible on the client.
Prefix it with NEXT_PUBLIC_ to expose it.
Enter fullscreen mode Exit fullscreen mode
Build-Time Validation
Wrap your Next.js config:
// next.config.ts
import { withCtroEnv } from "@ctroenv/nextjs"
export default withCtroEnv(schema, nextConfig)
Enter fullscreen mode Exit fullscreen mode
Validates at config load time — before the build starts.
Accessing Secrets
Server secrets are masked ("********"). Use meta.get() for raw values:
env.JWT_SECRET // "********"
env.meta.get("JWT_SECRET") // actual value
Enter fullscreen mode Exit fullscreen mode
Choosing an Adapter
Runtime Adapter Source Best for Node.js@ctroenv/node
.env files + process.env
APIs, CLIs, servers
Vite
@ctroenv/vite
import.meta.env
Frontend apps, SSG
Next.js
@ctroenv/nextjs
Server/client split
Full-stack apps
Cloudflare Workers
core’s workersSource()
Worker env binding
Edge functions
Deno/Bun
core’s detectSource()
Auto-detected
Cross-runtime apps
All adapters use the same schema. Switch between them by changing the source.
npm install @ctroenv/node @ctroenv/vite @ctroenv/nextjs
Enter fullscreen mode Exit fullscreen mode
Previous: Type-Safe Env Vars Without Zod
Next: Testing and Debugging Your Env Config
답글 남기기