Learn Build a Real SaaS with Claude Code Git and PR Workflow with Claude Code

Git and PR Workflow with Claude Code

Intermediate 🕐 20 min Lesson 4 of 25
What you'll learn
  • Explain why one large AI-generated diff is harder to review carefully than several small, sequential ones
  • Read this track's real 13-commit history and identify why each commit is scoped the way it is
  • Apply a one-sentence test for whether a unit of work should be one commit or split into two
  • Ask Claude Code to show the actual diff before committing, not just a prose summary of what it changed
  • Explain how the schema-then-auth-then-routes-then-UI ordering in the reference build reflects real dependency order, not arbitrary sequencing
  • Describe the refactor-then-bugfix pattern from commits 11 and 12 and why small commits make it possible

One-Shot Generation Is the Wrong Model

It is tempting to treat an AI coding agent like a vending machine: describe the whole app in one long prompt, get a finished app back. That approach works for toy demos and fails for anything you intend to actually maintain, because a single giant diff is nearly impossible to review carefully -- you either rubber-stamp it or you do not really review it at all. The reference build for this track took the opposite approach: thirteen commits, each one a coherent, reviewable unit of work, built and committed in sequence over one extended session.

The Real Commit History

Here is the actual, unedited sequence from the reference repo's git log --oneline --reverse:

# Commit message
1
Initial commit from Create Next App
2
Add Drizzle schema and migrations for businesses, reviews, responses
3
Add Supabase auth: login/signup page, session proxy, server/browser clients
4
Add seed script with realistic mock review data across 3 platforms
5
Add in-memory rate limiter with Vitest unit tests
6
Add review dashboard UI and ingestion/response API routes
7
Add Stripe billing: free/pro tiers, checkout, portal, webhook
8
Add Resend email: weekly digest and negative-review alerts
9
Add loading/error states and a responsive pass on the dashboard
10
Add Playwright e2e coverage for the unauthenticated user journey
11
Extract Stripe subscription reconciliation into a pure, testable function
12
Fix: stale Stripe webhook events could downgrade resubscribed customers
13
Document setup, testing, Stripe/Resend usage, and Vercel deploy steps

Read that sequence as a story, not a list. Schema before auth. Auth before a seed script that needs accounts to attach to. A rate limiter with its own tests before the API routes that depend on it. The UI comes after the routes it calls, not before. Commits 11 and 12 are the most instructive pair in the whole history -- more on those below.

Sizing a Commit

Notice that no commit in that list tries to do two unrelated things. "Add Stripe billing" does not also touch the dashboard UI. "Add review dashboard UI and ingestion/response API routes" pairs together because the UI in that commit literally cannot function without those routes -- that is a legitimate single unit, not scope creep. The test is simple: could you describe this commit's purpose in one sentence without using the word "and" to join two unrelated things? If not, it is two commits.

Commit what we just built as a single commit: the Drizzle schema and the initial migration for businesses, reviews, and responses. Use a message describing what the schema adds, not how we got here.

Reviewing the Diff, Not Just Reading the Summary

After Claude Code finishes a unit of work, it will tell you what it did in prose. That summary is not a substitute for looking at the actual diff. Ask explicitly:

Show me the diff for everything you just changed before we commit.

This is where you catch the things a summary glosses over: a TODO left in code, an environment variable referenced but not added to .env.example, a query that is not actually scoped to the right business. Reviewing diffs incrementally, commit by commit, is dramatically easier than trying to review thirteen commits' worth of change in one sitting at the end -- by the time you would notice a problem from commit 3, you have forgotten what commit 3 was supposed to do.

The Bug-Fix Pair: Commits 11 and 12

This pair is worth studying closely because it shows the workflow paying off in the way it is supposed to. Commit 11, "Extract Stripe subscription reconciliation into a pure, testable function," is a refactor with no behavior change -- pulling webhook-handling logic out of the route handler into a function that takes plain inputs and returns a plain output, specifically so it could be unit tested. Commit 12, "Fix: stale Stripe webhook events could downgrade resubscribed customers," is the bug that refactor's tests then caught: Stripe can deliver webhook events out of order, and a naive handler could let an old "canceled" event overwrite a newer "active" status. The full story, including the failing test and the fix, is documented in the repo's DEBUGGING_STORY.md.

That sequence -- refactor for testability, then a test catches a real bug -- almost never happens in a one-shot generation, because there is no natural pause between "feature built" and "feature shipped" for a test to run in. Small commits create that pause on purpose.

A Practical Loop

The repeatable pattern behind all thirteen commits: ask Claude Code for one coherent unit of work, read the diff, ask follow-up questions about anything unclear, request changes if needed, then commit with a message describing the what and why, not a transcript of the conversation. Repeat. This is slower per-commit than one giant prompt and faster overall, because you are never debugging a thirteen-feature tangle to find which feature broke something.

With the workflow established, lesson 5 starts building on the scaffold from lesson 3 -- Supabase auth, the first real feature in the build.

Key takeaways
  • The reference build is 13 sequential, reviewed commits, not one generation -- schema before auth, a rate limiter (with its own tests) before the routes that use it, UI after the routes it calls
  • A commit whose purpose needs the word and to describe two unrelated things should usually be split into two commits
  • Asking Claude Code to show the diff before committing catches problems a prose summary glosses over -- leftover TODOs, missing env vars, queries scoped to the wrong record
  • Commit 11 (extract reconciliation logic into a testable function) and commit 12 (the bug that refactor's tests then caught) show small, sequential commits creating room for tests to catch real bugs
  • Stripe webhook events can arrive out of order -- a naive handler can let a stale canceled event downgrade a customer who already resubscribed; see DEBUGGING_STORY.md in the reference repo for the full incident
  • Reviewing diffs commit by commit as you go is dramatically easier than reviewing a whole multi-feature build in one sitting at the end