CogniPrep is a psychometric test practice site running on Next.js and Vercel. Until this week, if you had cloned the repository and asked "which Node does this deploy on?", there was no honest answer. The answer was a dropdown in a project settings page that nobody had opened in months.
The commit that fixed it touches three files and is almost entirely lockfile. The interesting part is which of the two version files does which job, because they are not interchangeable.
.nvmrc was stale and production never read it
--- a/.nvmrc
+++ b/.nvmrc
-v22.22.2
+v24.22.1
+ "engines": {
+ "node": "24.x"
+ },
Vercel's documented inputs for the Node version are the project setting and engines.node in package.json, with engines overriding the dashboard. .nvmrc is not one of them. So before this commit, the repository contained an exact patch version that production ignored, pointing at a release line production was not on, and that is a worse state than having no file at all: it reads like documentation.
The other half of that page explains why engines is a range and not 24.22.1:
Only major versions are available. Vercel automatically rolls out minor and patch updates when needed, such as to fix a security issue.
You cannot pin a patch on Vercel even if you want to. 24.x is the whole vocabulary. So the division of labour is: engines declares the major the deploy must use, and .nvmrc declares the exact patch a laptop should use, because nvm use is the only consumer that can act on that precision.
Why 24 and not 22, which is also LTS
The Node previous releases table currently lists v26 as Current, v24 (Krypton) as the latest LTS, v22 (Jod) as LTS, and v25 as end of life since March. Vercel offers exactly 24.x (its default) and 22.x. One of those two is on the way out and one is the current LTS, so the choice makes itself.
The patch is not cosmetic either. 24.22.1 is the 21 October 2026 security release: ten CVEs, four of them high severity, the worst an out-of-bounds write in the bundled Brotli decoder (CVE-2026-111497), with the same fixes shipped as 22.23.4 and 26.12.1 and no published workaround for any of the ten. Vercel will roll that patch out underneath us whether or not we ask, which is the right default. But a project whose runtime version lives in a dashboard has nowhere to write down that it cares.
The types were describing a dead release line
One semver range and 40 lines of pnpm-lock.yaml:
- "@types/node": "^25.9.5",
+ "@types/node": "^24.19.2",
@types/node at 25 was describing a Node line that reached end of life in March 2026, while the code ships on 24. That is backwards in a quiet way: the type definitions were the most current thing in the repository and the least connected to anything that runs. Types ahead of your runtime do not catch bugs, they hide them, because every API added after your runtime typechecks clean and then throws TypeError: not a function in a serverless function at 2am.
Dependency versions that describe an environment are worth treating differently from dependency versions that are just "latest wins". For this one, newest is simply wrong.
The pin announces itself, and I am leaving it noisy
Here is the part nobody mentions when they tell you to add engines. My laptop is on Node 26, so every single script now opens with this:
$ pnpm typecheck
[WARN] Unsupported engine: wanted: {"node":"24.x"} (current: {"node":"v26.5.0","pnpm":"11.18.0"})
$ tsc --noEmit
A warning, not an error, because pnpm only hard-fails an engine mismatch under engine-strict. A line of yellow on every command is a cheap, permanent reminder that local and production disagree, and it is strictly better than the previous arrangement, where they could disagree silently for a year and the only record of the disagreement was a screenshot in somebody's settings tab.
See it, or rather, fail to see it
The subject of this post is something you cannot observe from outside, and you can check that you cannot:
curl -sI https://cogniprep.app/api/health
curl -s https://cogniprep.app/api/health
The headers give you server: Vercel, an x-vercel-id, HSTS, a content security policy and no runtime version. The body is a real health check, so the numbers move:
{"status":"healthy","timestamp":"2026-10-24T08:12:38.840Z","checks":{"database":{"status":"healthy","responseTime":247},"redis":{"status":"configured"},"environment":{"status":"healthy"},"monitoring":{"status":"configured"}}}
Four checks, a live database round trip in milliseconds, and not one word about which Node is running. That is the correct design for a public endpoint, since publishing your runtime version to the internet is free reconnaissance for anyone reading a CVE list. Vercel's own suggestion for answering the question is to log process.version during the build, which tells you about one deployment and nobody else.
Which leaves the repository as the only place the answer can live. It is in there now, in two files, each doing the one job it is capable of.
The thing the runtime is actually carrying is at cogniprep.app: practice tests for 56 assessment providers. The health endpoint above is public, so you can watch that database response time change while you read.
Top comments (0)