Your Stripe checkout works.
Your billing probably doesn't. Not yet.

Stripe delivers webhooks at least once and in no guaranteed order. Between "test payment succeeded" and real customers, most integrations hit the same bugs: double-provisioning, a duplicate credit, a paid user without access.

Webhook-Proof Stripe is the billing layer built for client products, packaged for your codebase in an afternoon.

Launch price $59 for the first 50 buyers — enter code LAUNCH59 at checkout

What goes wrong — and what the kit does

Stripe delivers each event at least once, in no guaranteed order. Watch a typical handler on the left and Webhook-Proof Stripe on the right.

Stripe sends the same invoice.paid event twice. A typical handler credits twice. The kit ledger marks the second delivery as duplicate, so it is never queued twice.

Typical integration

Stripe
Your app
DB
  • invoice.paid evt_1A
  • invoice.paid evt_1A

Same event again. No dedupe.

credits 2 · emails 2

With Webhook-Proof Stripe

Stripe
Kit endpoint
DB
  • invoice.paid evt_1A
  • invoice.paid evt_1A

Second row conflicts. Marked duplicate.

duplicate · not queued twice

How it works

Acknowledge first. Workers claim events with a time-limited lease. Only the worker holding the lease can commit, so a stale or crashed attempt's database writes are discarded. Trust Stripe's live object, not the webhook payload.

  1. 01

    Stripe

    Events arrive at least once, in no guaranteed order.

  2. 02

    Verify

    The endpoint verifies the Stripe signature, enforces a 1MB body cap, and checks livemode. It ignores event types the kit doesn't handle and replies 200.

  3. 03

    Ledger

    It stores each event once in an event ledger: INSERT ... ON CONFLICT DO NOTHING.

  4. 04

    200 OK

    Stripe gets its 200 as soon as the event is safely stored, before any processing.

  5. 05

    Workers

    3 workers by default (FastAPI); on Next, after() plus a cron drain. Events are claimed with FOR UPDATE SKIP LOCKED plus a lease. Events are deduplicated by Stripe event id, so a duplicate delivery is never queued twice.

  6. 06

    Re-fetch

    Workers re-fetch the object from Stripe instead of trusting the payload or the arrival order. Synced state updates when a worker processes the event; failures retry with backoff.

  7. 07

    Refunds and disputes

    Refund and dispute events are recorded and synced from Stripe.

  8. 08

    Your DB + hasAccess(customerId, feature?) / has_access

    hasAccess reads your synced rows. Disputes and fully refunded subscription periods revoke automatically; Payment Link refunds go to a review queue (REFUND_REVOKE_POLICY).

Retry / dead-letter

Failures retry with exponential backoff; after repeated failures, it moves to a dead-letter list. On Vercel, events are processed right after the response via after(), with a cron drain as backup. FastAPI runs in-process workers or python -m app.worker.

What you don't have to build

The kit is the billing layer, not a new app. You drop it in. You don't re-derive webhook correctness.

  • Signature verification and raw-body gotchas per framework

    Included

  • Idempotent event ledger (INSERT … ON CONFLICT DO NOTHING)

    Included

  • Lease-based worker queue with retries and a dead-letter list, claimed with FOR UPDATE SKIP LOCKED plus a lease

    Included

  • Out-of-order handling by re-fetching from Stripe

    Included

  • Retries, exponential backoff, and a dead-letter list

    Included

  • Refund and dispute events are recorded in the ledger and synced from Stripe.

    Included

  • Entitlements via hasAccess(customerId, feature?) / has_access from synced state

    Included

  • Payment Links fulfilment

    Included

  • Customer Portal

    Included

  • Tests with real signed events against real Postgres

    Included

  • The 16-chapter field guide

    Included

The route you write

Left is the handler most apps ship. Right is the kit: verify, ledger, 200, then workers.

Typical Next.js route
export async function POST(req: Request) {
  const event = await req.json() // parses JSON before verifying
  const invoice = event.data.object
  await db.credits.add(invoice.customer, 1) // no dedupe
  await mailer.sendWelcome(invoice.customer) // trusts the payload
  await provisionAccount(invoice.customer) // slow work before 200
  return Response.json({ received: true })
}
From the kit: app/api/stripe/webhook/route.ts
/**
 * Accept-fast Stripe webhook. Workers drain the ledger after the response.
 */
import { after, NextResponse } from 'next/server';
import { billingPolicy } from '@/lib/stripe/config';
import { acceptWebhook } from '@/lib/stripe/webhook-handler';
import { drainPendingEvents } from '@/lib/stripe/webhook-worker';

// Runs on the Node.js runtime (the default): raw body, Node crypto and the postgres driver.
// Don't add `export const runtime`: Next 16 rejects route segment config under
// `cacheComponents` (next.config.ts), and the Edge runtime can't run this route.

/**
 * Chunked bodies carry no Content-Length: stop reading once the cap is passed, so memory
 * stays bounded at the cap plus one chunk. Returns null when the body is too large.
 */
async function readBodyWithCap(req: Request, maxBytes: number): Promise<string | null> {
  if (!req.body) {
    return '';
  }
  const reader = req.body.getReader();
  const chunks: Uint8Array[] = [];
  let received = 0;
  for (;;) {
    const { done, value } = await reader.read();
    if (done) break;
    received += value.byteLength;
    if (received > maxBytes) {
      await reader.cancel();
      return null;
    }
    chunks.push(value);
  }
  return Buffer.concat(chunks).toString('utf8');
}

export async function POST(req: Request) {
  const contentLengthHeader = req.headers.get('content-length');
  if (
    contentLengthHeader &&
    Number(contentLengthHeader) > billingPolicy.maxWebhookBodyBytes
  ) {
    return NextResponse.json({ error: 'payload_too_large' }, { status: 413 });
  }

  const body = await readBodyWithCap(req, billingPolicy.maxWebhookBodyBytes);
  if (body === null) {
    return NextResponse.json({ error: 'payload_too_large' }, { status: 413 });
  }
  const signature = req.headers.get('stripe-signature');
  const accepted = await acceptWebhook(body, signature, billingPolicy.maxWebhookBodyBytes);

  if (accepted.status === 200 && accepted.body.queued === true && process.env.VITEST !== 'true') {
    after(() => {
      void drainPendingEvents(billingPolicy.drainBatchSize);
    });
  }

  return NextResponse.json(accepted.body, { status: accepted.status });
}

Not another SaaS boilerplate

Boilerplates give you a whole new app: auth, landing page, blog, and a Stripe integration somewhere inside. That's great on day zero.

This kit is for everyone else: you already have an app (or a client does) and you need Stripe done properly inside it. No auth opinions, no UI framework lock-in, no rewrite.

What's inside

Two production modules, one behaviour

  • ✓Next.js App Router module (TypeScript, Drizzle + Postgres)
  • ✓FastAPI module (async SQLAlchemy 2 + Postgres, Alembic migrations)
  • ✓Shared schema for customers, subscriptions, payments, invoices, and the event ledger. Run a Next.js front end on a FastAPI back end without mismatches.

Webhooks that survive the real world

  • ✓Raw-body signature verification, with the Next.js and FastAPI gotchas handled
  • ✓The endpoint verifies the Stripe signature, enforces a 1MB body cap, and checks livemode
  • ✓Event ledger: INSERT ... ON CONFLICT DO NOTHING. Stripe gets its 200 as soon as the event is safely stored, before any processing.
  • ✓Workers re-fetch the object from Stripe instead of trusting the payload or the arrival order
  • ✓Failures retry with exponential backoff; after repeated failures, it moves to a dead-letter list

The whole Stripe surface, not just Checkout

  • ✓Subscriptions: trials, monthly/annual, upgrades and downgrades via Customer Portal, cancel-at-period-end, past_due grace
  • ✓One-time payments: Checkout and PaymentIntent webhook handling, including async payment methods
  • ✓Payment Links fulfilment: sell with a no-code link and still provision inside your app
  • ✓Invoices: create with idempotency keys
  • ✓Refunds and disputes: recorded and synced from Stripe
  • ✓Idempotency keys on invoice creation
  • ✓Entitlements helper: hasAccess(customerId, feature?) / has_access, derived from synced state

Proof, not promises

  • ✓Unit and integration tests with real signed events against real Postgres, on both stacks
  • ✓One-command "chaos replay" script against your own local endpoint
  • ✓CI workflow included

The 16-chapter field guide (Markdown)

  • 📘Checkout vs Payment Links vs Invoices: a decision table
  • 📘The subscription states that matter, and what access each one grants
  • 📘A go-live checklist
  • 📘Incident runbook: "Stripe says active, my app says free". Diagnose it in 10 minutes.
  • 📘Porting guide for Next.js-only, FastAPI-only, and split stacks

Who it's for

  • →Founders and indie hackers with a working product who are about to switch on payments
  • →Contractors and agencies who integrate Stripe for clients and want one tested pattern they can reuse
  • →Python teams tired of every Stripe kit being Next.js-only
  • →Teams with existing Stripe and a quiet suspicion their DB and Stripe disagree

Who it's not for

  • ✕You want a full SaaS template with auth, landing page and blog. Buy a boilerplate; this kit will sit happily inside it later.
  • ✕You need Stripe Connect / marketplaces today (on the roadmap)

Pricing

Solo

$79one-time

Launch price: $59 for the first 50 buyers — enter code LAUNCH59 at checkout

Prices in USD. Taxes calculated at checkout.

  • ✓Next.js + FastAPI modules, tests, and field guide
  • ✓For one individual developer and their own projects
  • ✓12 months of updates
  • ✓14-day refund guarantee
Buy Solo License
Best for agencies

Agency

$199one-time

Prices in USD. Taxes calculated at checkout.

  • ✓Everything in Solo
  • ✓Unlimited client projects. No per-client license needed.
  • ✓One written async review of your existing webhook handler (send a repo link or snippet; reply within 5 business days)
  • ✓Priority email support
Buy Agency License

Frequently Asked Questions

Is this a SaaS boilerplate?

No. It's a billing module you add to an existing Next.js or FastAPI app. It has no auth, UI kit or landing page, so it fits whatever you already use.

Which versions does it support?

Next.js 16 App Router, Python 3.12 with FastAPI, Postgres, and the Stripe API version pinned in the repo. Updates follow Stripe API changes for 12 months.

I use Prisma / SQLModel / MySQL. Will it work?

The ledger and queue use Postgres features (ON CONFLICT, FOR UPDATE SKIP LOCKED plus a lease); the DB layer is spread across several files, so swapping ORMs is real work.

Does it work with Supabase / Neon / RDS?

Yes. It's plain Postgres.

Edge runtime?

The webhook route runs on the Node.js runtime on purpose. The guide explains why.

Can I use it in client projects?

On the Agency tier, yes, unlimited. Solo is for one individual developer and their own projects.

How is it delivered?

A ZIP download immediately after purchase, plus GitHub repo access on request.

What if it doesn't work for me?

Email me within 14 days if it doesn't work as described and I'll refund you. See our refund policy.

Do you offer implementation help?

The Agency tier includes one written review. For hands-on integration work, I take a limited number of contract projects: contact me.

About me

I'm Ashwani Arya, a contract full-stack engineer (React, Next.js, Node.js, Python/FastAPI) based in Bangalore, India. I build products for startups, and Stripe is the part I get called in to make correct: PaymentIntents, Payment Links, Invoices, API integrations and webhook handling in production.

This kit is the pattern I reach for, written fresh and documented properly.

Stop guessing whether your billing is right. Prove it.

Get Webhook-Proof Stripe — $79 ($59 with launch code)

Launch price $59 for the first 50 buyers — enter code LAUNCH59 at checkout