DEV Community

Cover image for Framework-Specific Env Patterns
Odejobi Abiola Samuel
Odejobi Abiola Samuel

Posted on

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() })
Enter fullscreen mode Exit fullscreen mode

loadEnv() resolves files in order:

  1. .env — shared defaults
  2. .env.{NODE_ENV} — environment-specific (.env.development, .env.production)
  3. .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

Links: GitHub · Docs · npm

Previous: Type-Safe Env Vars Without Zod
Next: Testing and Debugging Your Env Config

Top comments (0)