Stripe subscriptions in Next.js: the hard part is after checkout
Taking the payment is the easy half of a Stripe integration. Redirect to Checkout, collect the card, done. The hard half is everything after: keeping one Stripe customer per user, not granting access twice when Stripe retries a webhook, revoking it when a charge is refunded, and answering the one question your whole product depends on. Is this user actually subscribed? Most Stripe tutorials for Next.js stop before any of that.
It's also the half you can verify. The starter this post walks through passes 57 automated checks on a clean clone, with real payments run through the exact code in Stripe test mode. That's the bar this stuff should clear. Here's what breaks after Checkout, and what fixing it actually takes.
How do you check if a user has an active subscription?
Read a local mirror of the subscription, not Stripe's API on every request. When a webhook tells you a subscription changed, write its status to your own database. Then gating is one indexed query: does this user have an active row? Calling Stripe's API on every page load is slow, rate-limited, and unavailable exactly when Stripe is having a bad day.
The clean shape is three small functions. A getSubscription() that reads the mirror, a requireActiveSubscription() guard you put in front of paid routes, and a hasPurchased() for one-time products. The detail tutorials miss is that "active" isn't a boolean. A subscription that someone cancelled should keep access until the period they already paid for ends, and a trialing subscription counts as active. Gate on the real lifecycle state, not on whether a row merely exists.
How do you stop a retried webhook from granting access twice?
Record every event id and skip the ones you've already handled. Stripe delivers events at least once and retries a failing endpoint for hours, so the same event can arrive more than once. If your handler grants an entitlement without checking whether it's seen that event before, a routine retry double-grants it. Store event.id with a unique constraint, and make a duplicate a no-op.
There's a second webhook trap that's easy to hit and hard to debug. Signature verification runs on the raw request body. If your framework parses the body to JSON first, verification fails on bodies that were perfectly valid, and you'll chase it for a day. Verify the raw body first, then parse. Next.js needs the route configured so the body arrives unparsed.
What happens to access when someone cancels or refunds?
Two different events, two different answers, and getting them backwards is the most common billing bug there is.
A cancellation keeps access until the end of the current period. The customer paid through the 30th, so they keep it through the 30th, and your gate should read the period end rather than revoking the moment the cancel event lands. A refund is the opposite. When you handle charge.refunded, revoke access, because the money went back. A sensible starter keeps access on a partial refund and pulls it on a full one. If you gate purely on "has a subscription row," a refunded customer keeps what they stopped paying for, forever.
The smaller traps that still cost you a weekend
None of these are exotic. Each one is a specific thing that works in the demo and breaks with real users.
| The trap | What breaks | The fix |
|---|---|---|
| Every checkout makes a new Stripe customer | One person's billing splits across many customer records | One customer per user, created once and reused, with an idempotency key so two concurrent checkouts can't make two |
| A subscribed user picks another plan | They get a second subscription billing in parallel | Switch the existing subscription in place and let Stripe prorate |
| The price on your page drifts from Stripe | You show $9 and charge $12 | A check that fails the build on any mismatch between your pricing config and Stripe |
| Users can write their own billing rows | Someone sets their own plan to "pro" in the database | Row-level security and column grants make billing tables read-only to users. Only the webhook writes |
Can you buy this instead of building it?
Yes, and for most teams shipping a product it's the better call. Stripe Starter is a Next.js template that ships all of the above: Next.js 15, React 19, TypeScript, Supabase, Drizzle, Stripe, and Tailwind v4. You pay once for a non-exclusive, worldwide, perpetual license to use, modify, and deploy the software. You own your copy and run it on your own infrastructure.
What's in the box is the part that took the original Builder the longest. Subscription checkout with optional free trials, one-time purchases alongside, the Stripe customer portal for cancellations and card changes, the gating functions above, the webhook deduplication, and billing tables that only the webhook can write. It also ships an AGENTS.md runbook, an ordered setup guide written to hand straight to a coding agent, with every key and dashboard step and a failure-symptom table.
Two honest limits before you buy. Maintenance is none. It's sold as-is, so you own upkeep after the purchase, which is fine for infrastructure this stable but worth knowing. And it's fixed-price and one-time products only. No tax handling, no metered or usage billing, no multi-seat, no email sending, no admin dashboard. Open the demo and run the 57 checks yourself before you decide.
Is it safe to buy a codebase from someone you don't know?
It's safe when the code is delivered, checkable, and the money can be pulled back if it was misrepresented. Every sale is final, and the source code is delivered the moment you pay. You then have 24 hours to report a factual problem, misrepresentation or a bug that stops it working, and the Builder's payout is held for 72 hours so a report gets reviewed before any money moves. Identity is verified on both sides, and every listing has to carry a working demo, so you check the real thing first. For the full list of what goes wrong when you buy small software, read is it safe to buy a SaaS business.
When building it yourself is the right call
Buying isn't always the move.
- You need billing this template leaves out. Metered or usage-based billing, multi-seat, or built-in tax all mean real work on top, and at that point a from-scratch build may be cleaner.
- You already have billing. If you just need one more endpoint, you don't need a whole starter.
- You want a maintainer. This is sold as-is with no updates. If you want someone shipping fixes, that's a different kind of purchase.
Common questions
Does it handle one-time payments or only subscriptions? Both. Subscriptions with optional trials, and one-time purchases (lifetime deals) alongside them.
What's the database? Supabase Postgres with a Drizzle schema and migrations, and row-level security on every table.
Can users tamper with their own subscription status? No. The billing tables are read-only to users through row-level security and column grants. Only the verified webhook writes to them.
Do I get updates? No. It's sold as-is, and you own the code after purchase.
Open the demo before you think about price. Browse what Builders have listed on discover, and if your team keeps rebuilding the same billing plumbing on every project, buy software instead of building it makes the wider case. Who ends up buying and selling here covers all three sides.