⚡ Perfect for Vibe Coding — Skip weeks of setup. Browse 100+ production-ready boilerplates.

Browse boilerplates →

7 Stripe Subscription Gotchas That Break SaaS Apps (and How Boilerplates Pre-Solve Them)

James Park
9 min read 1,790 words

You followed the tutorial. Checkout works, the test card charges, the success page fires its confetti. Then you ship — and within the first few hundred customers, Stripe teaches you everything the tutorial skipped: a webhook arrives twice and grants two upgrades, a renewal fails silently and a churned user keeps full access for a month, a mid-cycle plan change produces an invoice that makes a customer email you the word "what."

This article assumes you've done the basics — it is deliberately not another "add Stripe to Next.js" walkthrough. It's the next search: the seven edge cases that actually break SaaS billing in production, what each one costs, and how production-grade integrations (the kind boilerplates ship) pre-solve them.

Why the happy-path tutorial isn't enough

Tutorial Stripe is a checkout. Production Stripe is a distributed system: your database and Stripe's each hold a version of the truth, connected by webhooks that are delivered at-least-once, in no guaranteed order, to an endpoint that might be down. Every gotcha below is some flavor of those two truths drifting apart — and drift in a billing system means money or access leaking, silently, in whichever direction hurts more.

The seven gotchas

1. Webhook reliability and idempotency

What breaks: Stripe delivers webhooks at-least-once — duplicates are normal, and your endpoint being briefly down means retries arrive minutes or hours later. Handlers written as "event arrives → apply change" double-apply on duplicates (two credit grants, two upgrade emails) and miss events entirely if the endpoint errored. Production handling: verify signatures, record processed event IDs and skip repeats, return 200 fast and process async, and reconcile periodically against Stripe's API for anything missed. This is table stakes, and almost no tutorial mentions any of it.

2. Out-of-order events

What breaks: invoice.paid can arrive before the customer.subscription.updated it relates to; a cancellation event can land before the update it supersedes. Handlers that assume narrative order write stale state over fresh state. Production handling: treat events as signals to re-fetch, not as the data itself — on any subscription event, pull the subscription's current state from the API and write that. One pattern, immune to ordering.

3. Failed payments and dunning

What breaks: renewals fail constantly in the wild — expired cards, insufficient funds, bank declines. Naive integrations either revoke access on first failure (enraging customers whose card simply rotated) or never revoke (handing out free months indefinitely — the silent revenue leak that compounds). Production handling: Stripe Smart Retries configured, dunning emails that actually send ("your card failed, update it here" recovers a large share of involuntary churn), a grace period, and then enforced downgrade. Every step needs wiring; none of it exists by default.

4. Proration on plan changes

What breaks: mid-cycle upgrades and downgrades generate prorated invoice items that surprise everyone — customers see charges they don't recognize; founders discover upgrades billed instantly while downgrades credited unexpectedly, or vice versa, depending on settings they never consciously chose. Production handling: deciding the policy (upgrade now + prorate, downgrade at period end is the sane default), configuring Stripe to match, and previewing the invoice to the customer before confirming. The gotcha is that Stripe has defaults, and defaults are decisions you didn't make.

5. Cancellations, refunds, and access revocation

What breaks: "cancel" is three different things — at period end (keep access until then), immediately (revoke now), and refund-then-cancel — and tutorials implement at most one. The classic bugs: cancelled customers losing access instantly despite paying through month-end, or cancellation never propagating so access outlives payment forever. Production handling: explicit handling of cancel_at_period_end, access checks driven by subscription state (not by "a cancellation event once happened"), and a refund path that also revokes. Boring, fiddly, and exactly where support tickets concentrate.

6. Your database vs. Stripe state

What breaks: the meta-gotcha behind half the others — apps that check Stripe live on every request (slow, rate-limited, down when Stripe is) or that cache subscription state once and let it rot. Drift here is the failure: your app grants access based on fiction. Production handling: a local subscription table as the single source your app reads, updated by the webhook/re-fetch pattern from #2, with a scheduled reconciliation sweep catching anything that slipped. This is the architecture tutorials skip entirely, and it's the one piece you really don't want to design after launch.

7. Tax, VAT, and the merchant-of-record question

What breaks: nothing in your code — something in your obligations. EU VAT applies from your first European customer; dozens of US states tax SaaS; each jurisdiction wants registration, collection, and filing. The tutorial said nothing because it's not an engineering problem, which is why it ambushes engineers. Production handling: either Stripe Tax wired in (calculation is automatic; registrations and filings remain yours) or selling through a merchant of record — Lemon Squeezy or Paddle resell your product and own the entire tax problem for a higher fee. At indie scale, MoR is routinely the right call; the directory tracks which boilerplates ship Lemon Squeezy and Paddle precisely because of this question.

Bonus gotcha — testing billing without real charges: if your test plan is "subscribe with the test card once," gotchas 1–6 are scheduled for production. The actual testing toolkit:

  • Stripe CLI forwards webhooks to localhost and — the underused part — resends past events on demand, which is how you test duplication and ordering without waiting for production to do it for you.
  • Test clocks fast-forward a simulated subscription through months of life in minutes: renewals, trial expirations, failed payments, the full dunning sequence. This is the only practical way to test gotcha #3 before customers do.
  • The special test cards are a curriculum in themselves: 4000 0000 0000 0341 attaches successfully then fails on charge (testing your dunning), others trigger disputes, insufficient-funds declines, and 3D Secure challenges. Ten minutes here teaches more than a week of happy-path clicks.
  • A staging environment with its own Stripe test account, so billing experiments never share state with production — and so the webhook endpoint mismatch (test events arriving at prod handlers, or vice versa) can't happen, which is its own classic gotcha.

What these gotchas cost in the wild

To make the stakes concrete, the three failure stories that recur constantly in founder post-mortems: the silent free tier — failed renewals never revoked access, discovered during a pricing audit eight months in, with the awkward choice between clawing back access from long-"paying" users or eating the loss. The double-grant incident — a webhook retry during a deploy double-credited annual plans, refundable only by hand, one apology email per customer. And the VAT letter — a European tax authority's polite inquiry about two years of uncollected VAT, which turns gotcha #7 from an abstraction into a number with interest attached. None of these announce themselves when the bug ships; all of them compound until noticed. That's the defining property of billing bugs and the reason this layer deserves paranoia the rest of your app doesn't.

Build it yourself vs. start from a tested integration

Everything above is buildable by hand — budget two to four weeks for a genuinely production-grade subscription system, plus the ongoing tax of tracking Stripe's API changes (the version-pinning treadmill is real). That's a fine trade if billing is your product's edge.

For everyone else, this is the strongest single argument for the boilerplate category: a maintained kit ships the webhook architecture, the state-sync table, the dunning flow, and the cancellation matrix as code that has already met thousands of production deployments — and, crucially, keeps getting patched as Stripe evolves. You inherit the edge-case fixes other people's customers discovered. (It's also why we recommend pairing your AI coding tools with a kit rather than generating billing from scratch — billing is the last place you want plausible improvisation, as anyone hardening an AI-built prototype learns at the webhook step.)

Boilerplates with production-grade billing, from the directory

Verified against current BoilerplateHub listings:

Stripe-native foundations: ShipFast ($129–149, Next.js) and SaaS Pegasus ($249–999, Django — with explicit subscription support) are the established picks; LaunchFast ($99–249, Astro/Next.js/SvelteKit) ships Stripe as well. The full, current list lives on the Stripe comparison page — every kit in the directory that ships it, with prices, side by side.

Merchant-of-record options (tax handled for you): supastarter and Larafast ship both Stripe and Lemon Squeezy, so you can choose per product; Shipped.club is Lemon Squeezy-native. The Lemon Squeezy comparison and Paddle comparison pages track the current field.

The cross-listing view is the part no vendor blog gives you: which foundations solve gotchas 1–6 in code and gotcha 7 by business model, at what price, on your stack — one table, kept current by the directory.

Frequently Asked Questions

Why do Stripe webhooks cause so many production bugs?

Because webhooks are at-least-once and unordered by design: duplicates are normal, retries arrive late, and related events land out of sequence. Handlers written from tutorials assume exactly-once, in-order delivery — so they double-apply changes, write stale state over fresh state, and miss events when the endpoint errors. Production integrations verify signatures, deduplicate by event ID, re-fetch current state rather than trusting event payloads, and reconcile on a schedule.

How should a SaaS handle failed subscription payments?

In layers: Stripe Smart Retries to recover the easy failures automatically, dunning emails prompting a card update (this alone recovers a meaningful share of involuntary churn), a defined grace period with access intact, then enforced downgrade and revocation. The two naive extremes — revoking on first failure or never revoking — respectively enrage paying customers and quietly give away free months at scale.

Do I need a merchant of record instead of plain Stripe?

If you sell internationally at indie scale, seriously consider it: VAT and US state sales-tax obligations begin with your first relevant customer, and with plain Stripe the registrations and filings are yours (Stripe Tax calculates but doesn't file). A merchant of record — Lemon Squeezy or Paddle — legally resells your product and owns the whole tax problem for roughly 1–2 points more in fees. Several boilerplates ship Lemon Squeezy support out of the box so the choice stays open.

Which boilerplates have the best Stripe integration?

Per current directory listings: ShipFast and SaaS Pegasus are the established Stripe-native picks (Pegasus with explicit subscription tooling for Django), with LaunchFast covering Astro/SvelteKit stacks; supastarter and Larafast ship both Stripe and Lemon Squeezy so you can defer the merchant-of-record decision. The Stripe comparison page shows every kit currently shipping it, side by side with prices — check it at decision time, since the directory updates as vendors ship.

BoilerplateHub BoilerplateHub ⚡ Perfect for Vibe Coding

You have the idea. Now get the code.

Save weeks of setup. Browse production-ready boilerplates with auth, billing, and email already wired up.

Comments

Leave a comment

0/2000