← All notes
SaaS · Payments
Stripe webhooks in Next.js App Router: the checklist that prevents silent churn
Every SaaS tutorial ends at the checkout redirect. Real life starts after: cards expire, customers dispute, upgrades happen inside the portal — and if your database doesn't hear about it, you either serve paying features to canceled users or lock out people who paid.
The non-negotiables
- Raw body for signature verification. App Router parses JSON by default, which corrupts signature checks. Read text first:
const payload = await req.text();
const event = stripe.webhooks.constructEvent(
payload,
req.headers.get("stripe-signature")!,
process.env.STRIPE_WEBHOOK_SECRET!
);
- Handle the lifecycle trio minimum:
checkout.session.completed, customer.subscription.updated, customer.subscription.deleted. Map all three to one DB write path that sets status + period end.
- Key your rows on customer ID, not session ID. Sessions are ephemeral; the customer object is the durable join key between Stripe and your users table.
- Return 200 fast, work async. Stripe retries on non-2xx with backoff. Slow handlers cause duplicate deliveries — make your handler idempotent (upsert by event or subscription ID) regardless.
- Local testing:
stripe listen --forward-to localhost:3000/api/stripe/webhook. The forwarding secret differs from production's — keep both in env.
Status mapping that survives edge cases
Treat a subscription as active only when status is active or trialing; everything else (past_due, canceled, incomplete_expired) means locked-but-graceful. Show the real state in-app — "payment failed, update card" converts better than a hard wall.
Test matrix before launch
- Successful subscribe → row flips active within seconds of redirect
- Card decline in Checkout → no active row created
- Cancel via test clock / portal → access ends at period end, not instantly
- Replay an old webhook → no duplicate rows (idempotency check)
- Wrong signature → 400, logged, nothing written
None of this is hard once built — it's just five places where "almost right" equals revenue leak.
Pre-built: my
AI SaaS Starter Kit implements this entire checklist on Next.js 15 App Router — signed webhooks, upsert-by-customer sync, graceful limit UX, build-verified. Swap the price ID and ship.