dotenv-expand
Expands variable references, command substitution, and encrypted values in dotenv-parsed .env files.
Repository Health
Technical Analysis
dotenv-expand extends dotenv with variable interpolation, letting one .env value reference another via ${VAR} or $VAR syntax, along with default and alternate value fallbacks. It also supports command substitution via $(command) and can decrypt dotenvx-encrypted values when a private key is present in the environment.
Built as a thin orchestration layer over the @dotenvx/primitives package, it’s typically chained directly after dotenv.config() to expand the parsed result before it’s written into process.env, making layered and cross-referencing .env configuration possible without introducing a full templating engine.
What You Get
- Variable expansion that resolves ${VAR} and unbraced $VAR references against process.env and already-expanded keys
- Default and alternate value syntax — ${MISSING:-default} and ${BASIC:+alternate} — for conditional fallbacks
- Command substitution — $(command) executes and inlines shell output into a value
- Encrypted value decryption — automatically decrypts dotenvx
encrypted:values when DOTENV_PRIVATE_KEY is set - Preload support via
node -r dotenv-expand/config, expanding .env before your app starts with no separate dotenv install required
Common Use Cases
- Composing DATABASE_URL from separately defined HOST/PORT/USER variables
- Preloading expanded env vars into a script via -r dotenv-expand/config in CI or npm scripts
- Deriving a value at load time with command substitution, e.g. capturing
node --versioninto an env var - Shipping dotenvx-encrypted .env files that only decrypt into expanded values when a DOTENV_PRIVATE_KEY is supplied at runtime
Under The Hood
Architecture
lib/main.js exports a single expand(options) function that takes options.parsed (the key-value map from dotenv.config()) and iterates each key while building up a runningParsed object for progressive expansion, so later variables can reference earlier ones already resolved within the same call. For each value it first checks a short-circuit case: if the target processEnv already holds a different value for that key, the pre-existing value wins untouched. Otherwise it builds a shadow copy of processEnv, strips entries that just mirror unexpanded literals (so already-expanded values from the running pass are preferred over raw pass-through data dotenv may have set), checks for a DOTENV_PRIVATE_KEY against dotenvx’s encrypted: convention and decrypts via @dotenvx/primitives, then calls that package’s expand and evaluate helpers for substitution and command evaluation. A thin config.js wrapper combines dotenv’s own CLI-arg/env-var option parsing with this expand() to power the -r dotenv-expand/config preload entrypoint. All substitution/decryption logic lives in the separate @dotenvx/primitives package; this repo is the orchestration layer over dotenv’s parsed output rather than a self-contained implementation.
Tech Stack
Plain CommonJS JavaScript with no framework — 'use strict', require/module.exports throughout. The only runtime dependency is the first-party @dotenvx/primitives package (pinned to 2.1.1 in devDependencies alongside dotenv ^17.4.2 for testing), which supplies decrypt, encrypted, evaluate, and expand. The build uses esbuild to bundle lib/main.js and config.js into dist/ as CJS targeting Node 16, with hand-written type declarations in lib/main.d.ts validated separately against tests/types via tsc. Linting is done with Biome rather than ESLint/Prettier. Deployment target is npm registry publish, with prepack running the esbuild build and prepublishOnly doing a dry-run pack check.
Code Quality
Tests live in tests/main.js using a small custom tap-style harness (tests/test.js), covering expansion, self-referential expansion (guarding against infinite recursion), default/alternate value syntax, encrypted-value decryption using @dotenvx/primitives’ encrypt/keypair helpers directly in the test file, and pre-existing process.env short-circuiting, with deliberate process.env = {} reset in beforeEach for isolation. npm test runs the harness directly with node, no external test-runner dependency. The pretest script chains Biome lint and a dts-check (tsc against tests/types/tsconfig.json), so type declarations are validated even though the library itself isn’t authored in TypeScript. Error handling is intentionally non-throwing — unexpandable or self-referential values resolve to an empty string rather than raising, a reasonable choice for a startup-time env loader that shouldn’t crash a caller’s boot sequence. Naming is short and consistent (_resolveEscapeSequences, runningParsed, literals).
API Design
The public surface is a single expand(options) function accepting { parsed, processEnv? }, mirroring dotenv’s own config() return shape so the two chain naturally: dotenvExpand.expand(dotenv.config()). This keeps boilerplate minimal — one extra line after the existing dotenv call — and the optional processEnv override lets callers redirect expansion into an object other than process.env without any additional API surface. Naming and option shapes deliberately parallel dotenv’s own conventions, reducing the learning curve for anyone already using dotenv. Documentation in the README is thorough, with worked examples for every feature (preload, command substitution, encrypted values, custom processEnv, interpolation syntax) presented as collapsible detail blocks.
Used by 14 apps in this directory
medusa
Ecommerce
The most flexible open-source commerce platform — build B2C, B2B, and marketplace applications with modular, composable commerce primitives.
opencloud
File Storage
Open source file management and collaboration platform that keeps your data under your control, no database required.
Payload CMS
Blogging · CMS · Developer Tools
The open-source, Next.js-native headless CMS that lives inside your /app folder and gives you a full TypeScript backend instantly.
Suna
AI Agents
Turn your company into a git repo — one config, one command center, a workforce of AI agents that runs the real work around the clock.
Vaultwarden
Password Manager · Security
Unofficial Bitwarden-compatible server in Rust — run the full Bitwarden ecosystem on a Raspberry Pi using every official client you already have, without the multi-container overhead.