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.
- Status: Approved
- Date: 2026-11-05
- Deciders: @puncoz
- Approved by: @puncoz
- Approved on: 2026-11-05
- Supersedes: ADR-0042 in part: decision 2's
ignoreCommand, and decision 5's Node 24 - Superseded by: —
- Design documents:
../sequence/build-and-search-sync.mdx,../architecture/system.mdx,../security/. Nothing a reader sees changes, so there is no app flow (ADR-0008).
Context
- ADR-0042 stopped previews twice:
git.deploymentEnabledgave branches other thanmainno deployment, and anignoreCommand,[ "$VERCEL_ENV" != production ], cancelled any deployment that still started outside production. - On GitHub, Vercel's status for both merges to
mainsince then (30cbb13, PR #5;d2752fa, PR #6) is "Canceled by Ignored Build Step". The merges beforevercel.jsonexisted (82ef827,e2f4ca7) show "Deployment has completed". Nothing onmainhas deployed since. - The owner confirmed that the production branch is
mainand that Automatically expose System Environment Variables is on. WhyVERCEL_ENVwas notproductionin 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.deploymentEnabledis Vercel's setting for which branches trigger a deployment. A branch that matches several rules deploys when at least one of them istrue.- 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.nodeto26.x(commit29978dc), Vercel's default, and asked for it to be recorded as approved.scripts/sync-content.tsis plain TypeScript run with Node's type stripping, which Node 26 has.
Decision
git.deploymentEnabledalone decides what deploys.vercel.jsonkeeps{ "**": false, "main": true }and has noignoreCommand. Every commit that lands onmain(pushed, merged or committed on GitHub) starts a production deployment, which Vercel assigns to the custom domain. Nothing else invercel.jsoncan skip or redirect it: its keys are only$schema,git,installCommandandbuildCommand. The dashboard's Ignored Build Step is Automatic.- 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
mainmatches the rule), or when someone runs avercelCLI deploy. Preview and Development hold no variables (ADR-0042 decision 6), soscripts/sync-content.tsrefuses (ADR-0042 decision 7). The build fails, and neither the index nor the domain changes. - Node is 26.
"engines": { "node": "26.x" }inpackage.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
| Option | Pros | Cons | Verdict |
|---|---|---|---|
deploymentEnabled only; the publish guard stops anything else from publishing | Vercel's own branch control; nothing extra can cancel main; a stray build fails loudly instead of in silence | An authorized fork build or a CLI deploy runs next build before the sync refuses | Chosen (owner) |
Keep ignoreCommand and find out why VERCEL_ENV was not production | Two rules, as ADR-0042 had | main stays undeployed until the cause is found from the owner's build logs | Rejected |
ignoreCommand on the branch, [ "$VERCEL_GIT_COMMIT_REF" != main ] | Does not read VERCEL_ENV | Reads the same system variables, so it may fail the same way; a fork's branch is also main | Rejected |
| Node 26.x | Vercel's default; what the owner chose | Not the Node on the owner's machine (v24) | Chosen (owner) |
| Node 24.x | Matches the owner's machine | Not the owner's choice | Rejected |
Consequences
Positive
- Every change on
mainreaches 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
vercelCLI, 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
mainpublishes, 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 buildwith 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:ignoreCommandremoved.$schema,git.deploymentEnabled,installCommandandbuildCommandunchanged. -
package.json:"engines": { "node": "26.x" }(the owner's commit29978dc). -
src/lib/vercel-config.test.tschecks four things: there is noignoreCommand;vercel.jsonhas only the four keys andgithas onlydeploymentEnabled; under Vercel's matching rule,maindeploys anddev,feature/search,main-old,release/main,Mainandstagingdo not; andpackage.jsonpins Node 26. TheignoreCommandtests are gone. - Not testable here: Vercel deploying the next commit on
mainto the custom domain. It is checked by the first push after this change. -
bun run test,bun run types:checkandbunx next buildpass.bun run lintstill 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: thevercel.jsontable and Node 26. -
content/docs/maintain/security/secrets.mdx:VERCEL_ENVis no longer read byvercel.json. -
content/docs/maintain/security/threat-model.mdxandchecklist.mdx: noignoreCommand; 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.
ADR-0042 — Only main deploys, straight to production on Vercel
The site is built and published by Vercel from main only: no preview deployments, one production build that also updates the search index.
ADR-0044 — Vercel installs with its own Bun, from the frozen lockfile
The Vercel build installs with bun install --frozen-lockfile, using the Bun Vercel picks from bun.lock, instead of fetching Bun through bunx.