ADR-0043 — Every change on main deploys to production, on Node 26

Vercel deploys every commit on main to the custom domain with no cancel rule in between, and the build runs on Node 26.

Context

  • ADR-0042 stopped previews twice: git.deploymentEnabled gave branches other than main no deployment, and an ignoreCommand, [ "$VERCEL_ENV" != production ], cancelled any deployment that still started outside production.
  • On GitHub, Vercel's status for both merges to main since then (30cbb13, PR #5; d2752fa, PR #6) is "Canceled by Ignored Build Step". The merges before vercel.json existed (82ef827, e2f4ca7) show "Deployment has completed". Nothing on main has deployed since.
  • The owner confirmed that the production branch is main and that Automatically expose System Environment Variables is on. Why VERCEL_ENV was not production in the cancel step cannot be seen from this repository. The command printed nothing, and the build log is in the owner's Vercel team.
  • The owner's requirement: a deployment starts if and only if something changes on main (a push, a merge or a commit), and it goes straight to the custom domain. There are no previews and no staging domain.
  • Read from Vercel's documentation (November 2026):
    • git.deploymentEnabled is Vercel's setting for which branches trigger a deployment. A branch that matches several rules deploys when at least one of them is true.
    • The production branch's deployments are production deployments, and the domains assigned to Production move to each new one.
    • Without an ignoreCommand, the dashboard's Ignored Build Step applies. Its Automatic behaviour builds every deployment that starts.
  • The owner moved engines.node to 26.x (commit 29978dc), Vercel's default, and asked for it to be recorded as approved. scripts/sync-content.ts is plain TypeScript run with Node's type stripping, which Node 26 has.

Decision

  1. git.deploymentEnabled alone decides what deploys. vercel.json keeps { "**": false, "main": true } and has no ignoreCommand. Every commit that lands on main (pushed, merged or committed on GitHub) starts a production deployment, which Vercel assigns to the custom domain. Nothing else in vercel.json can skip or redirect it: its keys are only $schema, git, installCommand and buildCommand. The dashboard's Ignored Build Step is Automatic.
  2. A deployment that starts anywhere else cannot publish. This happens when someone authorizes a fork's pull request under Git Fork Protection (a fork's branch named main matches the rule), or when someone runs a vercel CLI deploy. Preview and Development hold no variables (ADR-0042 decision 6), so scripts/sync-content.ts refuses (ADR-0042 decision 7). The build fails, and neither the index nor the domain changes.
  3. Node is 26. "engines": { "node": "26.x" } in package.json.

The rest of ADR-0042 stands: the history check, Bun 1.4.4 with --frozen-lockfile, Production-only variables, the publish guard, Git-only production deploys, revert to roll back, no tests in the build, protections on, and the owner's domain.

Options considered

OptionProsConsVerdict
deploymentEnabled only; the publish guard stops anything else from publishingVercel's own branch control; nothing extra can cancel main; a stray build fails loudly instead of in silenceAn authorized fork build or a CLI deploy runs next build before the sync refusesChosen (owner)
Keep ignoreCommand and find out why VERCEL_ENV was not productionTwo rules, as ADR-0042 hadmain stays undeployed until the cause is found from the owner's build logsRejected
ignoreCommand on the branch, [ "$VERCEL_GIT_COMMIT_REF" != main ]Does not read VERCEL_ENVReads the same system variables, so it may fail the same way; a fork's branch is also mainRejected
Node 26.xVercel's default; what the owner choseNot the Node on the owner's machine (v24)Chosen (owner)
Node 24.xMatches the owner's machineNot the owner's choiceRejected

Consequences

Positive

  • Every change on main reaches the custom domain with no cancel step in between.
  • One rule to read in vercel.json, and it is the one Vercel documents for this.

Negative / accepted trade-offs

  • A deployment that starts outside production is no longer cancelled before it builds. It fails at the sync instead and still counts against the deployment quota. Not authorizing fork deployments, and not running the vercel CLI, keeps this from happening.
  • A pull request from a fork's main (the owner's own workflow) shows Vercel's "Authorization required to deploy" on its head commit. This is Git Fork Protection, and that deployment is not wanted.
  • The owner's machine runs Node 24, and Vercel runs 26. Local checks run on 24.

Content invariants

  • §6.1 Private stays private: unchanged. Only main publishes, the same pages as before.
  • §6.5 Every page is readable as Markdown: unchanged.

Security considerations

  • What can publish is unchanged: a commit on main. The write key is still in Production only. A build that starts outside production, including an authorized fork's code, runs with no variables, so it has no write key and cannot write the index.
  • Fork code in a build. If a fork deployment is authorized, the fork's code runs during next build with the Preview environment, which is empty. This is the same as under ADR-0042, which cancelled it only once the build had started. Git Fork Protection stays on.
  • Residual risk: a failed, protected preview deployment exists if one is authorized. Its URL needs a Vercel login (Standard Protection).

Implementation notes

  • vercel.json: ignoreCommand removed. $schema, git.deploymentEnabled, installCommand and buildCommand unchanged.
  • package.json: "engines": { "node": "26.x" } (the owner's commit 29978dc).
  • src/lib/vercel-config.test.ts checks four things: there is no ignoreCommand; vercel.json has only the four keys and git has only deploymentEnabled; under Vercel's matching rule, main deploys and dev, feature/search, main-old, release/main, Main and staging do not; and package.json pins Node 26. The ignoreCommand tests are gone.
  • Not testable here: Vercel deploying the next commit on main to the custom domain. It is checked by the first push after this change.
  • bun run test, bun run types:check and bunx next build pass. bun run lint still fails with "typescript-eslint does not support TS 7.0".

Docs to update on implementation

  • content/docs/maintain/sequence/build-and-search-sync.mdx: the trigger and the first failure path.
  • content/docs/maintain/architecture/system.mdx: the vercel.json table and Node 26.
  • content/docs/maintain/security/secrets.mdx: VERCEL_ENV is no longer read by vercel.json.
  • content/docs/maintain/security/threat-model.mdx and checklist.mdx: no ignoreCommand; Node 26.
  • diagrams/docs/build-and-search-sync.{json,html}: Node 26.
  • AGENTS.md §1 and §5, README.md's Deploy section.
  • content/docs/maintain/adr/index.mdx: the row and the next free number. ADR-0042's Superseded by.

On this page

6 min read