Cheat SheetsFull-Stack IntegrationDeploying Both

Deploying Both — Cheat Sheet

Full-Stack Integration · 3 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Deploying Both
Full-Stack Integration3 topicsQuick revision reference
1

Deploying a Frontend and a Backend

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.

  • Vercel builds and serves the frontend with instant rollback; Fly runs the container with migrations as a release command
  • A failing release command aborts the deploy and leaves the previous version serving, which is the desired behaviour
  • Browsers keep running the previous bundle after a deploy, so the backend must ship first and additively
  • Rename fields with expand, migrate, contract across three releases rather than one breaking change
  • A migration does not roll back with a redeploy — additive changes are what make rollback possible at all
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.
2

Domains, Cookies and TLS in Production

The domain layout decides whether cookies work. Two subdomains of one site is a completely different situation from two unrelated hosts, and only the first lets a normal cookie through.

  • Same-origin governs CORS; same-site governs cookies — two localhost ports are same-site, two real domains usually are not
  • Putting the API on a subdomain of the app keeps cookies same-site and Lax working, which is the simplest layout
  • SameSite=None requires Secure and brings CSRF protection back into scope
  • A Domain attribute shares the cookie with every subdomain, including ones you may not control
  • A CSP connect-src that omits the API origin blocks your own fetches and looks exactly like a CORS failure
Subdomains are same-site; fly.dev is not
// SAME-ORIGIN  scheme + host + port identical    (CORS cares)
// SAME-SITE    the registrable domain matches         (COOKIES care)

aicancode.org  and  api.aicancode.org
//   different origins -> CORS applies
//   SAME SITE          -> a SameSite=Lax cookie IS sent

aicancode.org  and  tool-hub-api.fly.dev
//   different origins AND cross-site
//   -> a Lax cookie is NEVER sent. Auth silently fails.

// The three workable layouts:
//   1. api.aicancode.org  (a CNAME to Fly)
//      same-site, Lax works, cookies are simple. Best default.
//   2. aicancode.org/api  (proxied through Next rewrites)
//      same-ORIGIN, so no CORS either. Costs an extra hop.
//   3. a genuinely different domain
//      needs SameSite=None; Secure, plus CSRF protection, and
//      browsers increasingly restrict third-party cookies anyway.

// Layout 3 is where people end up by accident, by shipping with the
// platform-provided hostname. Set up the custom domain before
// launch, not after the first login bug.
3

Debugging Across the Seam

When something fails between two deployables, the first job is deciding which side it is on. A method for that is worth more than knowledge of any particular bug.

  • Check the network tab before forming a theory — no request row at all means the bug is client-side
  • A CORS error with no server headers is often a 500 that crashed before the CORS middleware ran
  • Postman and curl ignore CORS and send no Origin, so "works in Postman" always points at CORS or cookies
  • Server-rendered requests need cookies forwarded explicitly, which is why a 401 can appear only on first load
  • A request id echoed and logged on both ends is the single highest-value piece of full-stack observability
Network tab, status, curl, payload, logs
// 1. Did the request leave the browser?
//    Network tab, Fetch/XHR filter. No row at all -> a client bug:
//    a handler that never fired, a guard that returned early, a
//    URL built as "undefined/problems" from a missing env var.

// 2. What status came back?
//    (failed) with no status  -> network, DNS, CORS or CSP
//    401/403                  -> auth: check the Request Headers for
//                                the cookie or Authorization header
//    422                      -> read the body; it names the fields
//    5xx                      -> server side; go to the logs

// 3. Does curl reproduce it?
curl -i https://api.aicancode.org/problems \
     -H "Origin: https://aicancode.org" -b "sid=..."
//    curl works, browser fails  -> CORS, cookies, or CSP
//    curl fails too             -> a genuine server bug

// 4. Is it the payload? Compare what you SENT with what the API
//    expects. "Copy as cURL" from the network tab gives the exact
//    request, headers included.

// 5. Server logs, filtered by the request id from step 2.

// The single most useful habit: check the network tab before
// forming a theory. Most disagreements about whose bug it is end
// there.
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/full-stack