Lesson 2 — From green repo to a live production URL
Wires Vercel, Neon, env validation, and preview deployment protection on the starter, and walks the launch checklist to produce the production URL the rest of the chapter targets.
Across the last few chapters you assembled the four pieces of a shipping discipline — small reviewable PRs, a CI gate that goes green before merge, Vercel deploying on every push against a Neon branch per PR, and the expand-migrate-contract cadence for changing a schema you cannot take offline. This project cashes all of it in on one running app. The starter is the invoices SaaS you have grown across the course — the URL-state list, soft delete, version-based concurrency, all of it now sitting behind a Better Auth sign-in — and your job is two-part. First you ship it to a live *.vercel.app URL where the git push is the deploy and production is just an alias over an immutable build. Then you fix a schema anti-pattern it ships with: a single total numeric(12,2) NOT NULL column that mashes the line subtotal and the tax together, with no way to compute or display them apart. You split it into separate subtotal and tax columns through three reviewed PRs — expand, migrate, contract — across a live database, with the running app and the live schema never once incompatible. The chapter ends by rehearsing the production rollback against the most dangerous of those PRs, not to undo anything, but so the gesture lives in your fingers before an incident demands it at 2am.
The screenshot above is the destination, not a thing you read source to confirm. Each frame lives on a different surface — the running app, the provided inspector, and the Vercel dashboard — and you will not see them line up until you have shipped the first deploy and merged all three PRs. The inspector is the surface that makes the migration legible: a read-only observability panel, provided in full, that probes the live schema and the live data so every claim the cadence makes is something you can see rather than something you take on faith.
Everything below is a skill the later lessons build properly; this list is the map, not the teaching.
This is the shape of the machine, not its mechanics — the moving parts named, with the how left to the lessons that build each one. Six threads run through every lesson of this chapter.
main; every merge to main produces a production deployment against the production database. No human clicks “deploy.”pnpm db:migrate runs inside the build command against the PR’s Neon branch, so the build fails if the migration fails. You merge only after the rehearsal checklist is green on that preview.git revert on main, rehearsed against the contract PR while nothing is on fire./inspector, provided in full) — the read-only surface that makes all of the above visible: a schema-state probe, the split-coverage and dual-write panels, the data-integrity diff, and the deployment-environment plus build-source indicators.flowchart LR
subgraph rehearsal["Rehearsal stage — per PR"]
direction LR
push["git push<br/>(PR branch)"]
preview["preview deployment<br/>Neon branch off main"]
build["pnpm db:migrate<br/>&& next build"]
gate{"green CI +<br/>rehearsal<br/>checklist?"}
push --> preview
preview --> build
build --> gate
end
merge["merge to main"]
prod["production deployment<br/>Neon main branch"]
gate -- "yes" --> merge
merge --> prod
class push,merge action
class preview,build step
class gate check
class prod production
classDef action fill:#1f2937,stroke:#94a3b8,color:#f8fafc
classDef step fill:#dbeafe,stroke:#1d4ed8,color:#111
classDef check fill:#fef3c7,stroke:#b45309,color:#111,stroke-width:2px
classDef production fill:#dcfce7,stroke:#15803d,color:#111,stroke-width:2px The starter is the full invoices app, so most of the tree is code you already know and will not touch — the Better Auth flow, the CI workflow, the env validator, every shadcn component, and the entire inspector all ship provided. The annotated files below are the only ones the migration touches: the schema column shape and the surfaces that read or write it, plus the runbook stubs you fill as you go. Everything bolded is your focus. You write no inspector code — it exists so you can watch the migration, not so you can build it.
total only, with TODO(L3/L4/L5) markersdb + dbUnpooledwithTenant + tenantDb facadetotal; TODO(L4) dual-read coalesce, TODO(L5) drop totaltotal; TODO(L4) dual-write, TODO(L5) contractcombinedAmount helper — you create this in PR 2requireOrgUserResult<T> shapeTODO(L4) split inputs, TODO(L5) retire combined/api/health db ping@t3-oss/env-nextjs boundary — fails the build on a missing varLesson 2 — From green repo to a live production URL
Wires Vercel, Neon, env validation, and preview deployment protection on the starter, and walks the launch checklist to produce the production URL the rest of the chapter targets.
Lesson 3 — PR 1 (Expand): add the nullable subtotal and tax columns
Ships an additive-only migration adding subtotal and tax as nullable columns, and verifies the unchanged app stays healthy against the expanded schema.
Lesson 4 — PR 2 (Migrate): dual-write, backfill, dual-read
Lands the dual-write in the actions, the coalesce fall-through in the queries, the bounded-idempotent backfill, and the NOT NULL promotion — all while production keeps serving.
Lesson 5 — PR 3 (Contract): drop the old column, promote the new pair
Drops total, removes every legacy reference, and lands production on the target schema with the cadence’s safety claims intact.
Lesson 6 — Rollback rehearsal and the schema caveat
Promotes the previous deployment against the contract PR to make the “an alias rollback does not undo migrations” caveat concrete, then writes the durable runbook.
This lesson ends when the starter runs locally against a Docker Postgres. No accounts, no deploy yet — the Vercel project, the Neon integration, and the real environment values are all wired in the next lesson. The .env.example carries every key with a valid local placeholder, so nothing here needs an external account to boot.
Get the starter codebase from the project repository, under Chapter 100/start/.
Install dependencies.
pnpm installStart the local Postgres container, copy the environment template, then migrate and seed the database.
docker compose up -dcp .env.example .envpnpm db:migrate && pnpm db:seedStart the dev server.
pnpm devThe local environment keys, all pre-filled with valid placeholders in .env.example:
| Variable | Purpose |
| --- | --- |
| DATABASE_URL / DATABASE_URL_UNPOOLED | The Docker Postgres connection — pooled and unpooled, identical locally. The split matters on Neon, where migrations want the direct connection. |
| BETTER_AUTH_SECRET / BETTER_AUTH_URL | The dev-mode auth secret and base URL. |
| RESEND_API_KEY | Required by the env validator and the launch checklist, but this project sends no email — the placeholder is never called. |
| SENTRY_DSN | A launch-checklist value, not a wired package; the dev placeholder is enough to boot. |
| APP_URL | The app’s own base URL — http://localhost:3000 locally. |
| NEXT_PUBLIC_APP_NAME / NEXT_PUBLIC_APP_URL | The two client-exposed values. |
Expected result. pnpm dev serves the app at http://localhost:3000. Sign in as a seeded user — the seed creates two orgs and five users, all with the password inspector-password-12; alice@acme.test is an Acme admin and a good default. The invoices surface lives at /invoices and the inspector at /inspector, both reading the seeded total column, since you have not split it yet. No feature is built and nothing is deployed; the app is simply up in its starting state, which is exactly what the rest of the chapter changes.