Skip to content

Switching Deploys to Vercel: The Lock Is Outside the Code

Adityo Guni Waluyo

The diff was docs-only, yet the deploy migration from Cloudflare Workers to Vercel still happened. The wiring lives in the Vercel dashboard and GitHub settings, not in the repo.

TL;DR

A docs-only commit revealed the Workers-to-Vercel migration left almost no trace in Git, since the wiring lives in Vercel and GitHub settings. Vercel elects the production branch only at import, so the GitHub default branch stag and production branch main are two independent switches. Env variables need per-target re-registration, and rollback is repointing a deployment, not git revert.

Tonight I ran git show on the latest docs commit in the egperkasa company-profile repo. One line in CLAUDE.md had changed: the deploy target is no longer Cloudflare Workers, it is Vercel now, auto-deploying on every push. Meanwhile the .serena memory notes from the previous commit still carried the old Wrangler instructions.

My first guess was the obvious one: there must be heavy changes in wrangler.json. Adapter removed, build scripts reworked, that kind of thing. I scrolled the diff up and down. Nothing. Only documentation text had changed.

That is when it clicked, and my guess turned out to be completely wrong. A deploy migration like this leaves almost no trace inside the repo, because the wiring lives outside Git: in the Vercel dashboard and in GitHub repository settings. The repo only records the end state, like someone writing “house lock replaced” in a notebook while the actual handover happened at the notary.

The Production Branch Is Elected, Not Born

Old habit says the main branch automatically becomes production. On Vercel that seat is usually held by main, but not because of its name. When a new project is imported, Vercel picks the production branch in order: main first, then master if there is no main, and only then the repository's default branch [1]. Bitbucket has one extra step in that sequence before it falls through to the default branch, but that is outside my case.

Once the project exists, the seat can still move through Project Settings > Environments > Branch Tracking [1]. The consequence is simple: every branch outside the production branch only gets preview deployments [1], each with its own URL to check before merging.

So a GitHub default branch is not a crown. It is just one candidate in that election, and the election only happens at import time.

Why stag Became the Default on Both Remotes

On GitHub, the default branch is the base branch for pull requests and code commits [2]. Changing it requires admin access, and the repository must have more than one branch [2]. A detail I hit myself: my attempt to change the default branch on the second remote through the API was rejected because the PAT I used lacks the admin scope. I ended up doing it by hand in the web UI.

The end state is mechanical and boring, and that is the point. Someone cloning the repo lands on stag. New pull requests default to stag. The branch that absorbs most pushes is the one that only triggers previews, not the one that flips production. Even a mistake burns only a preview environment.

One nuance worth holding onto: changing the GitHub default branch does not automatically change the Vercel production branch of an existing project. That election happens at import; everything else is Branch Tracking. Two independent switches, so I set both explicitly: default branch stag, production branch main.

Two Remotes, One Rule

The repo now has two remotes: origin, wired to Vercel, and github-old, a pure backup mirror. This split is not a stylistic preference. Git's own documentation says the push URL and the fetch URL, although they can be set differently, must still refer to the same place; if you fetch from one place and push to another, use two separate remotes [4].

I was tempted to juggle push URLs on a single remote. I dropped the idea, because the documentation itself says that is not the road. Two remotes with clear responsibilities are far easier to audit: a push to origin is a deploy decision, a push to github-old is just backup.

What Changed from the Workers World

On Cloudflare Workers, the Worker code and static assets shipped as a single operation from the assets directory in the Wrangler config [3]. One command, one package, done. Moving to Vercel, several things that used to live in one config split into their own homes.

Environment variables are the clearest example. Vercel keeps variables per target: production, preview, development, down to branch-specific preview overrides, managed with vercel env ls or vercel env add [5]. Nothing migrates by itself; what used to sit neatly in the Cloudflare config has to be registered again, one by one.

Rollback changes shape too. Vercel describes instant rollback as pointing the domain at a different deployment [1], not a git revert. The mental model is closer to “point at the right version” than “roll the code back”.

The Git flow itself? Unchanged: feature into stag, releases through merges into main with a tag. What changed is only the answer to “what does this push trigger”.

That docs commit finally stopped feeling like leftover admin work. For a migration like this it is the only reasonable place to write things down. The lock itself is not something you can commit; what you can commit is the note about who holds the key.

Sources:

[1] Vercel: Deploying Git Repositories

[2] GitHub Docs: Changing the default branch

[3] Cloudflare Docs: Workers Static Assets

[4] git-scm: git-remote

[5] Vercel: Managing environment variables across environments

Related articles