diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 000000000..a45fd52cc
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+24
diff --git a/CLAUDE.md b/CLAUDE.md
index 19dfa9791..380fbcaa1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -54,7 +54,7 @@ npm run i18n:parity --workspace=shared # audit (exit 0)
npm run i18n:parity:strict --workspace=shared # CI gate (exit 1 on drift)
```
-Dev ports: the server listens on **3001**; the Vite dev server proxies the API, websocket, uploads, MCP and OAuth paths to it (see `client/vite.config.js`). There is no Node version pin — the CI workflows and the Dockerfile define what is tested. `server/.env.example` is the env-var reference.
+Dev ports: the server listens on **3001**; the Vite dev server proxies the API, websocket, uploads, MCP and OAuth paths to it (see `client/vite.config.js`). `.nvmrc` pins the Node major the Docker image runs and every CI job reads; `engines` in each package.json states the range that installs (npm only warns outside it). `server/.env.example` is the env-var reference.
## Server invariants
diff --git a/README.md b/README.md
index 9414f59ac..1000a57dd 100644
--- a/README.md
+++ b/README.md
@@ -223,7 +223,7 @@ Open `http://localhost:3000`. On first boot TREK seeds an admin account — if y
-
+



diff --git a/client/package.json b/client/package.json
index 704c3e003..944e97450 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,9 @@
{
"name": "@trek/client",
"version": "4.3.3",
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
+ },
"private": true,
"type": "module",
"scripts": {
diff --git a/package-lock.json b/package-lock.json
index 000506747..20c056762 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,9 @@
"concurrently": "^10.0.4",
"unrun": "^0.3.1"
},
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
+ },
"optionalDependencies": {
"@img/sharp-linuxmusl-arm64": "0.35.4",
"@img/sharp-linuxmusl-x64": "0.35.4",
@@ -98,6 +101,9 @@
"vite": "8.3.0",
"vite-plugin-pwa": "^1.3.0",
"vitest": "^4.1.11"
+ },
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
}
},
"client/node_modules/@vitejs/plugin-react": {
@@ -22015,6 +22021,9 @@
"typescript-eslint": "^8.70.0",
"unplugin-swc": "^1.6.0",
"vitest": "^4.1.11"
+ },
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
}
},
"server/node_modules/@eslint/config-array": {
@@ -22319,6 +22328,9 @@
"typescript": "^6.0.3",
"typescript-eslint": "^8.70.0",
"vitest": "^4.1.11"
+ },
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
}
},
"shared/node_modules/@oxc-project/types": {
diff --git a/package.json b/package.json
index 0cdcd6fd3..1e76d8c5e 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,9 @@
"name": "@trek/root",
"private": true,
"version": "4.3.3",
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
+ },
"workspaces": [
"client",
"server",
diff --git a/server/package.json b/server/package.json
index 7c5d3fd03..9745b8b41 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,9 @@
{
"name": "@trek/server",
"version": "4.3.3",
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
+ },
"main": "src/index.ts",
"scripts": {
"start": "node --require tsconfig-paths/register dist/index.js",
diff --git a/shared/package.json b/shared/package.json
index 804ada4be..e36461240 100644
--- a/shared/package.json
+++ b/shared/package.json
@@ -1,6 +1,9 @@
{
"name": "@trek/shared",
"version": "4.3.3",
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
+ },
"private": true,
"description": "Shared API contracts (Zod schemas) — single source of truth for TREK server and client.",
"type": "module",
diff --git a/wiki/Development-environment.md b/wiki/Development-environment.md
index c2488bae2..513c1016c 100644
--- a/wiki/Development-environment.md
+++ b/wiki/Development-environment.md
@@ -4,7 +4,7 @@
## Prerequisites
-- Node.js 22+
+- Node.js 24, the version in `.nvmrc` and the one the Docker image and CI run (`nvm use` or `fnm use` picks it up). 22.22.2 or later still installs, but nothing tests it.
- npm
- Git
- Python 3 and a C++ toolchain (`build-essential` on Debian/Ubuntu, the Xcode Command Line Tools on macOS, the Visual Studio Build Tools on Windows). better-sqlite3 ships prebuilt binaries, but a lockfile-driven `npm install` still runs node-gyp against them and stops without these (npm/cli#9837).