{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreihsxpgjns7st5opbt7wateuu6d557tveapto4d3cihe3eb5k6gjju",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpb6rcedxm72"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreigppvu35fk6ssfzoksnb7ckhbsmejwlhotee6auqvs7tj5a6qc2oe"
    },
    "mimeType": "image/webp",
    "size": 144498
  },
  "path": "/ctrotech/framework-specific-env-patterns-27e6",
  "publishedAt": "2026-06-27T09:43:39.000Z",
  "site": "https://dev.to",
  "tags": [
    "nextjs",
    "vite",
    "node",
    "tutorial",
    "GitHub",
    "Docs",
    "npm",
    "Testing and Debugging Your Env Config",
    "@ctroenv"
  ],
  "textContent": "Your schema is portable. But each runtime loads environment variables differently. CtroEnv adapters bridge the gap — same validation logic, different data sources.\n\n##  Node.js: process.env + .env Files\n\nThe `@ctroenv/node` adapter loads `.env` files and wraps `process.env`:\n\n\n\n    import { defineEnv, string, number } from \"@ctroenv/core\"\n    import { loadEnv } from \"@ctroenv/node\"\n\n    const env = defineEnv(schema, { source: loadEnv() })\n\n\n`loadEnv()` resolves files in order:\n\n  1. `.env` — shared defaults\n  2. `.env.{NODE_ENV}` — environment-specific (`.env.development`, `.env.production`)\n  3. `.env.local` — local overrides (gitignored)\n\n\n\nLater files override earlier ones. `process.env` takes precedence unless `override: true`.\n\n###  Monorepo Root\n\n\n    loadEnv({ path: \"../..\" }) // look up two directories for root .env\n\n\n###  Native Node 22+\n\nNode 22 has built-in `process.loadEnvFile()`. Use `native: true` to delegate:\n\n\n\n    loadEnv({ native: true }) // uses process.loadEnvFile() if available\n\n\nFalls back to the custom parser on older Node versions.\n\n###  System Fallback\n\nBy default, only file values are returned. With `system: true`, missing keys fall through to `process.env`:\n\n\n\n    loadEnv({ system: true })\n\n\n###  Standalone Parser\n\nUse `parseEnvFile()` directly for custom file loading:\n\n\n\n    import { parseEnvFile } from \"@ctroenv/node\"\n\n    const content = readFileSync(\".env.custom\", \"utf-8\")\n    const vars = parseEnvFile(content)\n\n\nHandles quotes, multiline values (backslash continuation), interpolation (`${VAR}`), comments, and `export` prefix.\n\n##  Vite: Build-Time Validation\n\nThe `@ctroenv/vite` plugin validates during the build:\n\n\n\n    // vite.config.ts\n    import { ctroenvPlugin } from \"@ctroenv/vite\"\n\n    export default defineConfig({\n      plugins: [\n        ctroenvPlugin({ schema: \"./src/env.ts\" }),\n      ],\n    })\n\n\nIf `DATABASE_URL` is missing, the build fails — no broken artifacts shipped.\n\n###  Schema Options\n\nPass a file path or inline definition:\n\n\n\n    // File path — imports the module, looks for `schema` export\n    ctroenvPlugin({ schema: \"./src/env.ts\" })\n\n    // Inline definition\n    ctroenvPlugin({\n      schema: {\n        DATABASE_URL: string().url(),\n        PORT: number().port().default(3000),\n      },\n    })\n\n\n###  Fail on Error\n\n\n    ctroenvPlugin({ schema: \"./src/env.ts\", failOnError: false })\n    // warns instead of failing — useful for optional env vars\n\n\n###  viteSource()\n\nUse with `defineEnv()` directly in Vite code:\n\n\n\n    import { defineEnv } from \"@ctroenv/core\"\n    import { viteSource } from \"@ctroenv/vite\"\n\n    const env = defineEnv(schema, { source: viteSource() })\n\n\n`viteSource()` reads from `import.meta.env` first, then falls back to `process.env`.\n\n##  Next.js: Server/Client Split\n\nNext.js bundles code for the browser. Server-only env vars must never reach the client bundle. The `@ctroenv/nextjs` adapter enforces this at runtime:\n\n\n\n    import { string, type ClientServerSchema } from \"@ctroenv/core\"\n    import { defineEnv } from \"@ctroenv/nextjs\"\n\n    const schema = {\n      server: {\n        DATABASE_URL: string().url(),\n        JWT_SECRET: string().min(32).secret(),\n      },\n      client: {\n        NEXT_PUBLIC_API_URL: string().url(),\n      },\n    } satisfies ClientServerSchema\n\n    const env = defineEnv(schema)\n\n\nServer components access everything. Client components can only access `NEXT_PUBLIC_` variables — accessing a server var throws:\n\n\n\n    Server-only environment variable \"DATABASE_URL\" is not accessible on the client.\n    Prefix it with NEXT_PUBLIC_ to expose it.\n\n\n###  Build-Time Validation\n\nWrap your Next.js config:\n\n\n\n    // next.config.ts\n    import { withCtroEnv } from \"@ctroenv/nextjs\"\n\n    export default withCtroEnv(schema, nextConfig)\n\n\nValidates at config load time — before the build starts.\n\n###  Accessing Secrets\n\nServer secrets are masked (`\"********\"`). Use `meta.get()` for raw values:\n\n\n\n    env.JWT_SECRET           // \"********\"\n    env.meta.get(\"JWT_SECRET\") // actual value\n\n\n##  Choosing an Adapter\n\nRuntime | Adapter | Source | Best for\n---|---|---|---\nNode.js | `@ctroenv/node` |  `.env` files + `process.env` | APIs, CLIs, servers\nVite | `@ctroenv/vite` | `import.meta.env` | Frontend apps, SSG\nNext.js | `@ctroenv/nextjs` | Server/client split | Full-stack apps\nCloudflare Workers | core's `workersSource()` | Worker env binding | Edge functions\nDeno/Bun | core's `detectSource()` | Auto-detected | Cross-runtime apps\n\nAll adapters use the same schema. Switch between them by changing the source.\n\n\n\n    npm install @ctroenv/node @ctroenv/vite @ctroenv/nextjs\n\n\n**Links:** GitHub · Docs · npm\n\n**Previous:Type-Safe Env Vars Without Zod**\n**Next: Testing and Debugging Your Env Config**",
  "title": "Framework-Specific Env Patterns"
}