Skip to content
Chapter 98Lesson 1

The push-is-the-deploy model

The mental model behind shipping to Vercel, how a git push becomes a deployment and why production is just an alias pointing at one immutable build.

You ended the last chapter with a green CI gate in front of a site nobody can reach yet. The four-job CI pipeline proves every commit on main typechecks, lints, tests, and builds, but proven code that no user can open is just a well-formatted private repo. “Going live” sounds like one big button you press once. On Vercel it isn’t a button at all. It’s a model, and you already half-know it, because the thing that triggers a deploy is a git push.

By the end of this lesson you’ll be able to look at any git push and predict exactly what Vercel does with it and where the result lands. That way, when you wire up the actual platform in the next lesson, every click confirms something you already understand instead of surprising you. This lesson writes no app code; it gives you the map that the rest of the chapter walks.

Start with the part you can see. A common first confusion with Vercel is “why did my pull request ship to production?” The cure is to notice that different git events go to different places. There are three, and the mapping is exact:

  • A push to main creates a production deployment.
  • A push to any other branch that has an open pull request creates a preview deployment: a build with its own generated *.vercel.app URL, posted as a comment on the PR.
  • Running the project locally, with vercel dev or with pnpm dev reading .env.local, is the development environment.

Three git situations, three kinds of deployment. What surprises people is how literal the mapping is. There’s no “promote to staging” ceremony and no separate publish step. Where your commit lands is decided entirely by which branch you pushed to and whether a PR is open.

git event push → main
git event push → feature/* (PR open)
git event vercel dev (local)
deployment Production deployment
deployment Preview deployment *.vercel.app
deployment Development
Each git situation maps to exactly one deployment kind.

One detail is worth slowing down on, because it carries the rest of the chapter: the code on all three can be byte-for-byte identical. The same commit, the same compiled output. What differs between production, preview, and development isn’t the code; it’s the environment the deployment runs in, which is to say the environment variables it reads. A preview and production can run the very same build and still behave differently, because one is talking to a test Stripe key and the other to a live one. Hold onto that distinction: environment is not code. We’ll come back to exactly how the split works later, so for now just plant the flag.

One more reframe, and it’s the one the chapter is named after. You might picture “the deploy” as an action you take, a thing you do once the code is ready. It isn’t. Vercel installs into your repo as a GitHub App that watches for pushes and creates deployments in response. It doesn’t push your application code anywhere; your git history is the source of truth, and Vercel reacts to it. The deploy isn’t a separate step you perform after the push. It is the consequence of the push, and that’s the whole idea behind “the push is the deploy.”

Sort each git action into the deployment environment it triggers. Watch the trap: not every push lands in production. Drag each item into the bucket it belongs to, then press Check.

Production The alias the custom domain points at
Preview A throwaway *.vercel.app build per PR branch
Development Your machine, reading .env.local
Merging a PR into main
Pushing a hotfix directly to main
Pushing a commit to an open PR branch
A second push to the same PR branch
Opening a new pull request
Running vercel dev on your laptop
Reading .env.local during pnpm dev

Deployments are immutable; production is just an alias

Section titled “Deployments are immutable; production is just an alias”

Everything else in the chapter hangs on this idea, so it’s worth your closest attention. Take it in two moves: first what a deployment is, then what “production” actually points at.

Every push produces a brand-new deployment: its own build, its own packaged output, its own permanent URL of the shape <hash>-<project>.vercel.app. Vercel never reaches back into an existing deployment to change it. There’s no in-place update and no overwrite. The build you shipped on Tuesday is still sitting there at its original URL on Friday, fully alive, exactly as it was. That’s what makes a deployment immutable : Vercel treats every build as a finished, frozen thing. It stays that way until you take it down. The only way an old deployment stops being reachable is if you explicitly delete it, which is also the one thing that removes it as a rollback target. Left alone, it lives indefinitely.

So if every push is a new, permanent deployment that never changes, what is “production”? Here’s the move that makes everything click: production is not a deployment at all. It’s a pointer. Your custom domain (app.example.com) and the project’s main *.vercel.app URL are an alias , a label aimed at exactly one deployment at a time. “Deploying to production” doesn’t mean a build becomes production. It means Vercel re-aims that pointer at a new deployment. The swap is atomic and effectively instant: no rebuild, no copy, nothing moves. A name stops pointing at one frozen value and starts pointing at another.

If that shape feels familiar, it should. Way back in “Bindings, not boxes,” you learned that a variable in JavaScript isn’t a box that holds a value. It’s a name bound to a value, and reassigning it just re-points the name. Production is the same idea wearing a domain name:

  • app.example.com is a let binding.
  • Each deployment is an immutable value, like a frozen object you can’t mutate.
  • “Deploy” is reassignment: app.example.com = <new deployment>.
  • The old values don’t vanish when you reassign. They’re just no longer the one the name points at.

That analogy is the keystone of the whole chapter. Picture the pointer moving while the values stay put: that picture is the entire model.

production alias app.example.com
a1b2 production
fix invoice total
a1b2-app.vercel.app
9f0e
add export button
9f0e-app.vercel.app
Two deployments exist. The production domain points at a1b2, the latest push to main.
production alias app.example.com
c3d4 built · not aliased
new pricing page
c3d4-app.vercel.app
a1b2 production
fix invoice total
a1b2-app.vercel.app
9f0e
add export button
9f0e-app.vercel.app
A new push to main builds c3d4. It's live on its own URL, but the domain hasn't moved.
production alias app.example.com
c3d4 production
new pricing page
c3d4-app.vercel.app
a1b2
fix invoice total
a1b2-app.vercel.app
9f0e
add export button
9f0e-app.vercel.app
Vercel re-aliases the domain to c3d4. The swap is atomic — no rebuild. a1b2 and the rest stay live at their URLs, ready to roll back to.

Two practical consequences fall straight out of this picture.

First, the payoff that justifies the whole model: because every previous deployment is still alive at its own URL, rolling back is just re-aiming the pointer at one of them. No rebuild, no redeploy, no scramble. Vercel re-aliases the domain to a known-good deployment and you’re recovered in seconds. We don’t build the rollback flow here; that’s its own lesson later in the chapter, on instant rollback. But notice that you’ve already learned why it’s possible. Instant rollback isn’t a clever feature bolted on; it falls out of immutability plus an alias for free.

Second, a mistake that catches people constantly: a <hash>-<project>.vercel.app deployment URL is not “the app.” Each of those URLs is pinned to one specific deployment forever. The production alias moves between deployments; the deployment URLs never do. So if you copy a *.vercel.app URL out of the dashboard and send it to a colleague as “here’s our app,” you’ve handed them a frozen snapshot. Long after production has moved on, they’ll still be staring at whatever build happened to be there that day. Share the production domain, never a deployment URL.

You know now that a push produces an immutable deployment and that going live is a pointer swap. What actually runs between the push and the swap? Treat it as a black box with labeled stages. You rarely need to look inside, but you do need to know the order and one relationship that catches almost everyone.

In order, when you push to main, Vercel:

  1. Fetches the pushed commit.
  2. Runs the install, pnpm install, inside a fresh Linux container.
  3. Runs the build, pnpm build / next build, in that container, with environment variables scoped to the target environment (production vars for a main push, preview vars for a PR push).
  4. Packages the output into Functions artifacts : bundled server functions plus static assets.
  5. Deploys it globally, live on its own deployment URL.
  6. Re-aliases the production domain to the new deployment, the pointer swap from the previous section.

Notice where the alias swap sits: dead last, after the deployment is already live on its own URL. The deployment is live on a URL before it’s aliased to production, and that gap is exactly what rollback exploits. The deployment exists and works before it’s ever the one production points at.

Now the relationship that’s worth getting crisp, because the wrong version of it leads to real trouble. You might assume your CI gate from the last chapter protects production, that a push to main can’t ship until the four jobs go green. It doesn’t, and by default it can’t. The Vercel deploy and the GitHub Actions CI run in parallel and independently. The deploy does not wait for CI. A push to main can re-alias production to a new build before, or even while, CI is still running. Green CI is not a precondition for the production swap unless you deliberately make it one.

So what was your CI gate protecting? Merges. The branch ruleset blocks a broken PR from merging into main, and that’s what indirectly keeps main healthy, which keeps production healthy. The protection is real, but it lives one step upstream of the deploy. When you genuinely need the deploy itself to wait on CI, the tool is Vercel Deployment Checks , which hold a freshly built deployment un-aliased until your external checks pass, so production never points at a build that hasn’t gone green. We name it here and wire it up later. The distinction to carry away: your ruleset gates merges to main, not the production alias.

The other thing this pipeline forces you to understand is when an environment variable is read, because it’s not always when you’d guess, and the mismatch is the most common “I changed the env var and nothing happened” bug.

  • Some variables are read at build time and baked into the artifact: anything prefixed NEXT_PUBLIC_* (those get inlined directly into the client bundles), the env.ts validator, anything used during static generation.
  • Other variables are read at runtime from the running environment: server-only secrets that your server actions and route handlers read when a request comes in.

The consequence is sharp, and it’s just the immutability rule again in a new costume. The artifact is frozen, so anything baked into it at build time is frozen too. Change a NEXT_PUBLIC_* value in the dashboard and the deployments already built still carry the old value, soldered into their bundles. The only way the new value takes effect is a rebuild, which means a new deployment. A runtime secret, by contrast, is re-read on the next request, so it can change without a rebuild. When you change a public variable and the live site doesn’t budge, you haven’t hit a bug. You’ve hit immutability, and the fix is to ship a new build.

Order what Vercel does, from the moment you push to `main` until users see the new version. Watch the last two: a deployment is live on its own URL *before* the production alias ever moves. Drag the items into the correct order, then press Check.

Fetch the pushed commit
Run pnpm install in a fresh Linux container
Run pnpm build with the target environment’s variables
Package the output into Functions artifacts
Deploy the new artifact to its own URL
Re-alias the production domain to the new deployment

The fastest way to a bad first week in production is to assume “deploy” means “everything is now in sync.” It doesn’t. A deploy ships code and nothing else, and the worst incidents live in the gap between “code shipped” and “system actually consistent.” Three pieces of that gap are worth naming outright:

  • No database migration runs. Drizzle migrations are a separate, deliberate step, and the entire next chapter is about running them safely. Shipping code that expects a new column does not create the column. The deploy that references it just starts failing against a schema that doesn’t have it yet.
  • No external CDN gets purged. If you put a cache in front of Vercel (a Cloudflare layer, for instance, covered later in this chapter), it has its own cache with its own lifetime. A new deploy doesn’t reach through and clear it.
  • No secrets rotate. A deploy never touches your credentials. Rotating a leaked key is its own action, on its own schedule.

The principle that ties all three together is worth saying plainly: the deploy ships code, and everything stateful is your responsibility. Databases, caches, secrets, anything with a lifetime of its own: Vercel will happily ship code that assumes those are in a state they’re not in. Knowing the gap before you ship is the difference between a planned migration and a 2 a.m. page.

Git drives the normal path: you push, Vercel deploys, and you never open a terminal. The CLI is for the off-path moments, the times you need to step outside the git flow on purpose. The useful way to learn it isn’t an exhaustive flag reference; it’s knowing when you’d reach for each command.

Terminal window
vercel
vercel --prod
vercel env pull .env.local
vercel logs <url>
vercel inspect <url>
vercel promote <url>
  • vercel deploys the current directory as a preview, by hand, off the git flow. Handy for a quick throwaway build without opening a PR.
  • vercel --prod deploys to production straight from your machine. Reach for this almost never: it bypasses PR review and CI entirely, putting whatever is on your laptop in front of users with no gate in between. It’s listed mainly so you recognize it as the wrong tool in normal work.
  • vercel env pull .env.local syncs the development-scoped variables down to your local .env.local. This is the command every developer runs right after cloning the repo so their local environment matches. We’ll set it up properly in the next lesson.
  • vercel logs <url> streams a deployment’s logs, your first move when a specific deployment is misbehaving.
  • vercel inspect <url> shows a deployment’s metadata and build output, for digging into why a build came out the way it did.
  • vercel promote <url> re-aliases production to a specific existing deployment. This is the durable, scriptable version of the pointer swap, and the primitive that instant rollback is built on. We use it in earnest in the rollback lesson later in the chapter.

The whole list collapses to one rule: reach for the CLI during an emergency rollback, for local dev with production-shaped variables, or to inspect a failed build, and otherwise let git drive.

You could run this stack elsewhere. Cloudflare Pages with Workers, Netlify, AWS Amplify, and a self-hosted Node server all deploy Next.js, and any of them can host a real SaaS. The course commits to Vercel for one specific reason: in 2026, Next.js 16’s runtime features are co-developed with Vercel, so the framework and the platform move together. New framework capabilities land on Vercel first and fit without friction. That tight coupling is the bet the course makes, and it won’t revisit it from here on.

That’s the map. Everything that follows takes this model and wires it into the actual platform. The next lesson lands your first real production URL; later lessons put the function in the right region, attach a custom domain, give every preview its own database, scope your environment variables, build the instant-rollback flow this lesson promised, and finish with a launch checklist. Each one is a click or a config, and because you have the model now, each one will land as a confirmation instead of a surprise.