Skip to content

chore(config): replace dotenv with Node's native env file loader - #750

Merged
chrispaskvan merged 1 commit into
mainfrom
chore/native-env-file
Sep 27, 2026
Merged

chrispaskvan merged 1 commit into
mainfrom
chore/native-env-file

Conversation

@chrispaskvan

Copy link
Copy Markdown
Owner

What

Node 26 loads env files natively, so the -r dotenv/config preload is redundant. This swaps it for --env-file-if-exists=.env in the three scripts that used it and drops the dependency.

dotenv was only ever wired in through those preload flags — there is no import 'dotenv' or dotenv.config() anywhere in the source, and nothing else in the tree depends on it. The .env file itself is 7 lines of plain KEY=value with one comment: no ${VAR} expansion, no quoting, no multiline values, nothing the native parser lacks.

Why --env-file-if-exists and not --env-file

The file is absent in both places that matter — .env is listed in .dockerignore, so the production image never has one, and it is gitignored, so CI never checks one out. Plain --env-file exits 9 on a missing file. The if-exists form exits 0 with a one-line notice on stderr, which matches dotenv's silent no-op closely enough to be a drop-in.

Behavior verified on Node 26.8.1

Case Result
Clean env, .env present Loads PORT, DOMAIN, PROTOCOL from the file
Variable already in environment Shell value wins — same as dotenv's non-override default
.env missing Exit 0, notice on stderr, startup continues
Full start:production flag set Parses; env loads before the permission model applies, so --permission does not block the read

That second row is what start:dev depends on when it sets NODE_ENV=development before delegating to start.

Side benefit

dotenv v17+ prints an ◇ injected env (N) from .env banner to stdout — the same stream pino writes structured JSON to. The native loader is silent, so startup output stays machine-parseable.

Trade-off

CI and the container now print .env not found. Continuing without it. to stderr on every run, where dotenv was silent. Cosmetic, and there is no flag to suppress it.

Testing

pnpm test — 62 files, 992 passed, 1 skipped. Also run by the pre-push hook.

🤖 Generated with Claude Code

https://claude.ai/code/session_018iD3V7Zyugg3atXVsogTBf

Node 26 loads env files natively, so the `-r dotenv/config` preload in the
start, start:production, and test:watch scripts is redundant. Swap it for
`--env-file-if-exists=.env` and drop the dependency.

`--env-file-if-exists` rather than `--env-file` because the file is absent
in both places that matter: `.env` is listed in .dockerignore, so the
production image never has one, and it is gitignored, so CI never checks
one out. Plain `--env-file` exits 9 on a missing file; the if-exists form
exits 0 with a one-line notice on stderr, matching dotenv's silent no-op.

Override semantics are unchanged - a variable already present in the
environment still wins over the file, which is what dotenv did by default
and what start:dev relies on when it sets NODE_ENV before calling start.

Side benefit: dotenv v17+ prints an `injected env (N) from .env` banner to
stdout, the same stream pino writes structured JSON to. The native loader
is silent, so startup output stays machine-parseable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iD3V7Zyugg3atXVsogTBf
@chrispaskvan

Copy link
Copy Markdown
Owner Author

Reviewed: a small, correct change with nothing blocking. Looks ready to merge.

Checked

  • No other dotenv use: nothing on the branch outside the lockfile mentions it (source, Dockerfile, CI, docs).
  • Node version: --env-file-if-exists needs Node 22.9+. engines, .nvmrc, the Dockerfile's NODE_VERSION and CI (via .nvmrc) are all on 26.8.1.
  • .env format: plain KEY=value lines and one comment, all within what the built-in reader handles.
  • Behaviour table: reproduced with the same flags on Node 26.8.1.
    • With .env present it loads, including under --permission --allow-fs-read=./.
    • A value already set in the shell wins over the file.
    • With .env missing it prints the notice to stderr and exits 0.
  • CI: green apart from Snyk Code, which is red on its test limit, same as on fix(users): support the intended Bungie platforms during first sign-in #749.

Optional: the production image never has a .env (it's in .dockerignore), so the flag could be dropped from start:production. That would stop the "not found" line appearing in every container's logs. Keeping it only matters if a .env is ever mounted into the container. Either choice is fine.

@chrispaskvan

Copy link
Copy Markdown
Owner Author

Thanks — agreed on the verification, and I re-ran the CI check independently: code/snyk is red on "Code test limit reached", a quota exhaustion rather than a finding against this diff, with CodeQL, analyze, codacy, lint/typecheck/openapi and security/snyk all green.

On the optional suggestion, the premise holds. start:production is invoked in exactly one place — the Dockerfile CMD — and nothing documents or runs it outside a container. Removing the flag also wouldn't disturb twilio/mms.service.spec.js:522, which parses that same script string; it only matches --allow-fs-read= and --allow-fs-write=.

One correction to the rationale for keeping it, though. The comment frames it as mattering "if a .env is ever mounted into the container" — but even then it would do nothing. All six keys in .env are declared as ENV in the Dockerfile (DESTINY_DATABASE_DIR, DESTINY2_DATABASE_DIR, DOMAIN, PORT, PROTOCOL, WEBSITE), and environment beats file under both the old loader and the new one. A mounted .env would be inert for every one of those keys regardless of the flag. It could only ever matter for keys the Dockerfile doesn't declare, and there are none today.

So the case for dropping is a little stronger than stated. The case against is one the comment doesn't raise: it would leave three sibling scripts where two carry the flag and one doesn't, for a reason package.json cannot explain, since JSON takes no comments. The payoff is a single stderr line per container start.

Proposing we merge as-is. The asymmetry costs more in future confusion than the log line costs in noise.

If the log line does turn out to be a nuisance, the clean version isn't just deleting the flag — it's deleting it and adding a comment in the Dockerfile beside the ENV block noting that production configures through ENV and so needs no env file. The TMPDIR block already sets that precedent. Happy to do that in a follow-up if it ever bites.

🤖 Reviewed with Claude Code · https://claude.ai/code/session_018iD3V7Zyugg3atXVsogTBf

@chrispaskvan
chrispaskvan merged commit a670950 into main Sep 27, 2026
5 of 6 checks passed
@chrispaskvan
chrispaskvan deleted the chore/native-env-file branch September 27, 2026 02:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant