@cogs/config
v0.4.0
Published
Framework-agnostic layered-dotenv loader — resolves the canonical .env file chain, expands ${VAR} references, and folds values into process.env with deploy-injected variables staying authoritative. Dual ESM+CJS so it works from an ESM --import preload or
Readme
@cogs/config
The framework-agnostic loader behind the canonical environment schema. It
resolves the layered .env file chain, expands ${VAR} references, and folds
the result into process.env — with deploy-injected variables staying
authoritative by default. One loader, one precedence, no per-repo forks.
Runtime dependencies: dotenv, dotenv-expand. Ships dual ESM + CJS so it
works from an ESM --import preload, a CJS --require hook (dd-trace and other
one-off tools that must load before anything else), and a plain
require('@cogs/config'). Node 20+ (the ESM --import preload needs ≥ 20.6;
the CJS --require path works on any Node 20).
See docs/ENVIRONMENT-CONFIG.md
for the full spec and ADR 0001
for the rationale.
Load order
Files are merged low → high precedence (later overrides earlier):
.env base defaults committed, non-secret
.env.local personal overrides gitignored (skipped when NODE_ENV=test)
.env.${ENV} stage config committed, non-secret
secretsPath injected secrets e.g. secrets.json (optional, parsed as JSON when *.json)
.env.${ENV}.local stage + personal gitignored${VAR}interpolation works across every file (dotenv-expand).- Default
override: false— a value already present inprocess.env(K8s ConfigMap, Vercel, CI) beats every file; dotenv files only fill gaps. Passoverride: truefor tooling that must force file values over the ambient environment. .env.localis skipped whenNODE_ENV === 'test'so personal-machine overrides never leak into a deterministic test run.${ENV}is the stage selector (dev|test|staging|production), orthogonal toNODE_ENV.
API
correctEnv(opts?)
Normalizes NODE_ENV, ENV, and BROWSERSLIST_ENV (reading BABEL_ENV),
keeping them mutually consistent. Existing process.env values always win over
the provided options. Run it first, before envs().
correctEnv({ nodeEnv: "development", env: "dev" });envs({ appDirectory, secretsPath?, override?, lint? })
Resolves the file chain above relative to appDirectory, parses + expands each
existing file, merges in precedence order, folds the result into process.env,
and returns it. .json files (e.g. the secretsPath) are parsed as JSON. lint
(default true) emits .env hygiene warnings.
envs({ appDirectory: __dirname });Lower-level helpers
parse, parseJSON, config (from dotenv-with-expand), the dotenvCheckers
/ dotenvKeyFixers / dotenvLineFixers hygiene rules, processEnv, and the
merge utilities (mergeDefaults, mergeAssign, resolveApp) are all exported
from the package root.
Usage
Preload for dev / start scripts
Populate process.env before any application module is evaluated:
// package.json
{
"scripts": {
"dev": "NODE_OPTIONS=\"--import @cogs/config/preload\" next dev",
"start": "NODE_OPTIONS=\"--import @cogs/config/preload\" next start"
}
}The ./preload entry side-effect-calls correctEnv() then
envs({ appDirectory: process.cwd(), secretsPath }), deriving secretsPath from
SECRETS_PATH (${SECRETS_PATH}secrets.json, or '' when unset). Requires
Node ≥ 20.6.
CJS preload (--require) — dd-trace and one-off tools
For a CJS --require hook (or an app server-preload.cjs), use the ./register
subpath — same side effect, loaded via CommonJS so it can sit alongside
dd-trace/init and other tools that must run before the app:
// package.json — order matters: env first, then tracer
{
"scripts": {
"start": "NODE_OPTIONS=\"--require @cogs/config/register --require dd-trace/init\" node server.js"
}
}Or from a server-preload.cjs that also does other CJS bootstrap:
// server-preload.cjs
require("@cogs/config/register"); // folds the .env chain into process.env
require("dd-trace").init();require('@cogs/config') also exposes the full API (correctEnv, envs, …)
from the CJS build for scripts that need to call the loader directly.
next.config.ts (build time)
Load env at the top of the config so the build sees resolved values:
import { correctEnv, envs } from "@cogs/config";
correctEnv();
envs({ appDirectory: __dirname });
export default {
/* next config */
};