Deploying a Frontend and a Backend
AdvancedTwo deployables means two pipelines, and the ordering between them matters: a backend change can break a frontend that is still serving the previous bundle.
Overview
A single deployable is deployed; two are coordinated. The frontend builds and goes out in a minute, the backend runs migrations and rolls instances, and for a window the two do not match — old bundles are still open in browsers, calling the new API. That window is why database changes have to be additive, and why "deploy the backend first" is a rule rather than a preference. The mechanics themselves are unremarkable: push to main, CI builds, Vercel serves the static half and Fly runs the container.
The Two Pipelines
What each side actually does on a push.
# Frontend on Vercel — no config needed for the common case
# push to main -> production build and deploy
# push to a branch -> a preview URL, per commit
# build command: next build; env vars per environment
# Rollback is instant, because previous builds stay served.
# Backend on Fly — a container, and migrations
# fly.toml
[deploy]
release_command = "alembic upgrade head" # runs BEFORE the new
# version takes traffic
[http_service]
internal_port = 8000
auto_stop_machines = "suspend"
min_machines_running = 1 # 0 means cold starts
[[http_service.checks]]
path = "/health" # unhealthy -> no traffic
# .github/workflows/deploy.yml
- run: flyctl deploy --remote-only
env: { FLY_API_TOKEN: '${{ secrets.FLY_API_TOKEN }}' }
# A release_command failure aborts the deploy and the old version
# keeps serving — which is exactly what you want from a bad migration.
# Health check discipline: /health should verify the database, and
# a deploy should not be considered done until it passes.Ordering and the Skew Window
Why old bundles keep running, and what that forces.
// After a deploy, browsers with the page already open keep running
// the PREVIOUS bundle — for minutes, or hours on a left-open tab.
// So for a while, old frontend + new backend are talking to each
// other. That window dictates the rules:
// 1. Deploy the BACKEND first, additively. The new API must still
// serve the old frontend.
// 2. Only then deploy the frontend that uses the new fields.
// 3. Remove the old fields in a LATER release, once no old bundle
// can still be running.
// The expand / migrate / contract sequence for a rename:
// release 1 add the new column, write to both, read the old
// release 2 backfill; frontend starts reading the new one
// release 3 stop writing the old; drop it
// Doing it in one step means every open tab breaks the moment the
// migration lands.
// Migrations that are safe online: ADD COLUMN (nullable, no default
// scan), CREATE INDEX CONCURRENTLY, adding a table.
// Migrations that lock: ALTER COLUMN TYPE, adding NOT NULL to a
// populated table, a non-concurrent index on a large table.
// Handle the stale bundle explicitly: a lazy chunk from the previous
// build 404s after deploy. Catch it and offer a reload rather than
// showing a broken page (see the React lazy-routes concept).Rollback and Confidence
What you need in place before the deploy that goes wrong.
# Frontend rollback is instant — promote a previous deployment.
vercel rollback <url>
# Backend rollback is a redeploy of the previous image:
flyctl releases -a tool-hub-api
flyctl deploy --image registry.fly.io/tool-hub-api@sha256:...
# But a migration does NOT roll back with it. A dropped column is
# gone, and the previous code expects it. This is the real reason
# migrations must be additive: rollback is only possible if the old
# code still works against the new schema.
# Before any risky release:
# - a database backup you have actually restored once
# - the migration tested against a copy of production data
# - a feature flag, so the new path can be turned off without a
# deploy
if flags.enabled("new_scoring", user): ...
# After every release, watch for five minutes:
# error rate, p95 latency, 4xx/5xx mix, and one business metric
# (signups, submissions) — the last catches "no errors, but nobody
# can complete checkout".
# Deploy on a Tuesday morning, not a Friday evening. The mechanism is
# not the risk; being unavailable when it misbehaves is.Key Points to Remember
- 1Vercel builds and serves the frontend with instant rollback; Fly runs the container with migrations as a release command
- 2A failing release command aborts the deploy and leaves the previous version serving, which is the desired behaviour
- 3Browsers keep running the previous bundle after a deploy, so the backend must ship first and additively
- 4Rename fields with expand, migrate, contract across three releases rather than one breaking change
- 5A migration does not roll back with a redeploy — additive changes are what make rollback possible at all
Interview Questions
Sign in to ask AriaWhy should the backend be deployed before the frontend?
How do you rename a database column without breaking a running frontend?
Why can you not simply roll back a deploy that included a migration?
Ask Aria about Deploying a Frontend and a Backend
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.