dotenv-expand

Expands variable references, command substitution, and encrypted values in dotenv-parsed .env files.

Library
npm
v1000.0.0
1,057 stars
BSD 3-Clause License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum. How we score it →
67 /100 Good
Development Activity 80
Maintenance 44
Community 56
Maturity 60
Momentum 28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation. How we score it →
70 /100 Good
Architecture 78
Code Quality 75
Innovation 65
Learning Curve 60

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 --version into 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

TypeScript
87%
MIT

medusa

Ecommerce

36,897

The most flexible open-source commerce platform — build B2C, B2B, and marketplace applications with modular, composable commerce primitives.

View details
94
Repo Health
87
Technical
63
Dependency
Built with
TypeScript 87%
JavaScript 12%
Updated 1 weeks ago
Go
52%
Apache 2.0

opencloud

File Storage

6,218

Open source file management and collaboration platform that keeps your data under your control, no database required.

View details
86
Repo Health
80
Technical
68
Dependency
Built with
Go 52%
Gherkin 33%
PHP 11%
Updated 1 weeks ago
TypeScript
96%
MIT

Payload CMS

Blogging · CMS · Developer Tools

45,458

The open-source, Next.js-native headless CMS that lives inside your /app folder and gives you a full TypeScript backend instantly.

View details
92
Repo Health
88
Technical
65
Dependency
Built with
TypeScript 96%
Updated 6 days ago
TypeScript
92%
Other

Suna

AI Agents

20,306

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.

View details
90
Repo Health
79
Technical
65
Dependency
Built with
TypeScript 92%
Updated 6 days ago
Rust
83%
AGPL 3.0

Vaultwarden

Password Manager · Security

69,556

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.

View details
89
Repo Health
69
Technical
69
Dependency
Built with
Rust 83%
Updated 1 weeks ago

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers