Skip to main content

41. Upgrade Node runtime to 24 LTS

Date: 2026-06-17

Status

Accepted

Context

The project was pinned to Node 20 in several places: .nvmrc (20), package.json engines (>=20), the devcontainer image (typescript-node:20), and prose in README.md, CLAUDE.md, AGENTS.md, and docs/go-live.md. CI consumes the version via actions/setup-node with node-version-file: .nvmrc, so .nvmrc is the single source of truth for the build/test runtime.

Two forces made this stale:

  1. Node 20 'Iron' has reached end-of-maintenance (~April 2026). As of June 2026 it no longer receives security patches. For an application handling PII and payroll/financial data — where this repo's stated posture is "security and correctness over speed" — running an EOL runtime is itself a risk.
  2. Modern tooling now expects a newer runtime (e.g. the Impeccable design skill requires Node ≥24), and the local development default had already been moved to Node 24 via Volta and nvm.

We considered three targets:

  • Node 22 'Jod' (Maintenance LTS) — supported but already in maintenance; upgrading to a soon-older line buys little.
  • Node 26 (latest Current, v26.3.0) — newest overall, but not LTS until ~October 2026. A Current release carries a ~6-month support window and no LTS stability/security guarantees. Inappropriate for a production financial app.
  • Node 24 'Krypton' (Active LTS, v24.16.0) — the current Active LTS through ~October 2026, with maintenance support continuing into 2027. Already the local default; satisfies the Node ≥24 tooling floor.

Decision

Standardize the project on Node 24 LTS, pinned to 24.16.0.

  • .nvmrc24.16.0 (CI follows automatically via node-version-file).
  • package.json engines.node>=24; add a Volta pin volta.node = 24.16.0 so Volta auto-selects the right runtime inside this repo (matching the wider org convention of pinning Node via Volta).
  • Devcontainer image → mcr.microsoft.com/devcontainers/typescript-node:24.
  • Update the Node version stated in README.md, CLAUDE.md, AGENTS.md, and docs/go-live.md.

The patch version is pinned exactly (not 24 or lts/*) so CI, local, and container builds are reproducible — important for an auditable payroll system. Patch bumps (e.g. 24.16.0 → 24.x.y for a security release) are therefore a deliberate, reviewed change to .nvmrc + volta.node, not automatic.

packageManager stays pnpm@9.12.0; no dependency changes are required (Next 16, React 19, Vitest 2, Biome 1.9, TypeScript 5 all support Node 24).

Consequences

  • Positive: back on a supported, security-patched LTS line; local, CI, and container runtimes are aligned to one exact version; unblocks Node ≥24 tooling.
  • Deploy follow-up (required): the host (Vercel) selects its runtime from its own project setting / engines.node. The Vercel project's Node.js version must be set to 24.x; verify in the Vercel dashboard so production matches CI. Tracked in docs/go-live.md step 3.
  • Contributor impact: developers need Node 24 locally. nvm use (reads .nvmrc) or Volta (reads volta.node) handle this automatically; the only manual step is installing 24 once.
  • Maintenance: exact pinning trades automatic patch uptake for reproducibility — security patch bumps must be applied manually. Revisit the target when Node 24 leaves Active LTS (~Oct 2026) or when 26 enters LTS.