Nub reads your .env* files and injects them into the environment before Node starts — no dotenv import, no --env-file flag. Loading happens from the nearest directory with a package.json (walking up from your cwd), matching Vite's single-directory model. Works on every Node version Nub supports (18.19+).

nub server.ts   # .env* in the project root are loaded automatically

File precedence

Four filenames are loaded, highest priority first. The shell environment always wins over all of them — a value already set in the process environment is never overridden.

  1. .env.[NODE_ENV].local
  2. .env.local
  3. .env.[NODE_ENV]
  4. .env

The [NODE_ENV] slots only exist when NODE_ENV is set, so with NODE_ENV=production the full set is .env.production.local, .env.local, .env.production, .env. Among the .env* files, the first one to define a key wins (first-writer-wins); the shell env sits above all of them.

NODE_ENV=production nub server.ts   # reads .env.production.local, .env.local, .env.production, .env

Test environment

Under NODE_ENV=test, the .env.local slot is skipped — only .env.test.local, .env.test, and .env load. This keeps developer-machine secrets in .env.local out of the test environment.

Variable expansion

Values support ${VAR} and $VAR references. References resolve against the other loaded values first, then the shell environment; an undefined reference resolves to the empty string. Expansion is multi-pass, so a value can reference another value that itself references a third.

# .env
HOST=localhost
PORT=5432
DATABASE_URL=postgres://${HOST}:${PORT}/app   # both forms work; $HOST is equivalent to ${HOST}

Escape a literal dollar sign with \$. Watch the classic footgun: a value like PASSWORD=foo$bar truncates to foo when bar is unset, since $bar expands to the empty string — quote and escape it as PASSWORD="foo\$bar".

Explicit files

Passing --env-file=<path> disables the automatic .env* discovery entirely — only the named file loads. Nub reads it through the same parser and the same ${VAR} expansion as the automatic files, and the shell environment still wins over it. This matches Bun: ask for a file by name and Nub stops guessing which files you meant.

nub --env-file=.env.ci server.ts   # only .env.ci loads; auto .env* discovery is skipped; shell env still wins