Home/Learn/Full-Stack Integration/Deploying a Frontend and a Backend

Deploying a Frontend and a Backend

Advanced
Deploying Both

Two 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.

Vercel builds; Fly migrates then rolls
# 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.

Backend first, additively, then contract later
// 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.

Additive migrations are what make rollback possible
# 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 Aria
1

Why should the backend be deployed before the frontend?

Medium
2

How do you rename a database column without breaking a running frontend?

Hard
3

Why can you not simply roll back a deploy that included a migration?

Hard

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.

Loading discussion…