CtroEnv
ctroenvType-Safe Environment Variables
Getting StartedQuick StartCore Concepts
defineEnv()string()number()boolean()pick()Chainable MethodsRefinementsError HandlingSchema Composition
CLI Overviewctroenv validatectroenv generatectroenv checkctroenv docsctroenv initCLI Configuration
Node AdapterVite AdapterNext.js Adapter
Migration from t3-envMigration from envalidMigration from dotenv

Node Adapter

Load .env files in Node.js with loadEnv and nodeSource for CtroEnv schemas.

  1. Docs
  2. Adapters

Node Adapter

The @ctroenv/node package provides .env file loading and process.env access for Node.js applications.

Installation

npm install @ctroenv/node

loadEnv()

Loads environment variables from .env files in a priority chain.

function loadEnv(opts?: LoadEnvOptions): EnvSource

Options

interface LoadEnvOptions {
  path?: string
  encoding?: BufferEncoding
  override?: boolean
  system?: boolean
}
OptionTypeDefaultDescription
pathstringprocess.cwd()Root directory to search for .env files
encodingBufferEncoding"utf-8"File encoding
overridebooleanfalseIf false, process.env takes priority
systembooleanfalseFall back to process.env when key not in files

File Priority Chain

Files are loaded in order, with later files overriding earlier ones:

  1. .env
  2. .env.{NODE_ENV} (e.g., .env.development)
  3. .env.local

Usage

import { defineEnv } from "@ctroenv/core"
import { loadEnv } from "@ctroenv/node"

const env = defineEnv(schema, {
  source: loadEnv(),
})

Override Behavior

By default, process.env takes priority over file values:

// process.env.DATABASE_URL always wins
const source = loadEnv()

// .env files override process.env
const source = loadEnv({ override: true })

nodeSource()

A simpler alternative that only wraps process.env:

function nodeSource(): EnvSource
import { defineEnv } from "@ctroenv/core"
import { nodeSource } from "@ctroenv/node"

const env = defineEnv(schema, {
  source: nodeSource(),
})

Unlike the default detectSource() (which checks import.meta.env first, then falls back to process.env), nodeSource() only reads from process.env. Use it when you want to skip the import.meta.env check.

parseEnvFile()

Parses a .env file string into key-value pairs. Supports the full .env file syntax.

function parseEnvFile(content: string): Record<string, string>

Features

| Feature | Example | Result | |---|---|---|---| | Comments | KEY=val # comment | { KEY: "val" } | | export prefix | export KEY=val | { KEY: "val" } | | Quoted values | KEY="hello world" | { KEY: "hello world" } | | Multiline (trailing \) | KEY=line one\
line two | { KEY: "line one\nline two" } | | Variable interpolation | URL=$BASE/api or URL=\${BASE}/api | Resolved from earlier keys, then process.env | | Escaped $ with $$ | KEY=price$$10 | { KEY: "price$10" } | | Escaped quotes | KEY=it's fine or KEY="say \"hi\"" | Preserves literal quotes |

Usage

import { parseEnvFile } from "@ctroenv/node"
import { readFileSync } from "node:fs"

const content = readFileSync(".env", "utf-8")
const env = parseEnvFile(content)
// env.DATABASE_URL => "postgres://localhost/db"

Interpolation Behavior

Variables are resolved from previously parsed keys in the same file, then falls back to process.env. Forward references are not supported — variables must be defined before they are used.

BASE_URL=http://localhost:3000
API_URL=${BASE_URL}/api/v1
# API_URL => "http://localhost:3000/api/v1"

EnvSource Type

The EnvSource type is re-exported for convenience:

import type { EnvSource } from "@ctroenv/node"

function customSource(): EnvSource {
  return {
    get(key: string): string | undefined {
      return process.env[key] ?? myCustomStore[key]
    },
  }
}

How is this guide?

Edit on GitHub

Last updated on Jun 24, 2026

PreviousCLI ConfigurationNextVite Adapter

On this page

InstallationloadEnv()OptionsFile Priority ChainUsageOverride BehaviornodeSource()parseEnvFile()FeaturesUsageInterpolation BehaviorEnvSource Type