Stripe SaaS boilerplate for Next.js (subscriptions + credits)
Stripe is the kind of integration that looks like a two-hour job in a tutorial and a two-week job in production. Webhook reliability, subscription state machines, customer portal sync, refund handling, and the difference between a `payment_succeeded` and a `subscription.updated` event are all real concerns. A Stripe boilerplate that handles them up front saves the sprint that would otherwise be spent learning Stripe the hard way.
SaaSForge Core, subscriptions for B2B SaaS
SaaSForge Core ships Stripe subscriptions with three tiers (Starter / Pro / Enterprise), monthly and yearly cycles, Stripe Checkout for new purchases, and Stripe Customer Portal for self-serve plan changes, cancellations, and payment method updates. Subscription state is mirrored into the workspace model so the app reads from its own database, not from Stripe on every request.
The webhook handler covers the lifecycle events that actually matter: `checkout.session.completed`, `customer.subscription.created`, `customer.subscription.updated`, `customer.subscription.deleted`, and `invoice.payment_failed`. Each branch is documented so you know what to extend versus what to leave alone.
SaaSForge AI, credits layered on subscriptions
SaaSForge AI uses the same subscription substrate and adds a credit economy on top. Each plan has a monthly credit allotment; the subscription's renewal webhook resets the balance; product actions (chat turns, embeddings, image generations) debit credits in proportion to their real token cost. The credit balance lives in your Postgres, not in Stripe.
The two billing models compose: a customer subscribes to a plan (Stripe handles the money), and the credit ledger meters their usage against the plan's allotment (your database handles the operational view). Upgrading to a larger plan tops up credits immediately; cancelling keeps credits until period end.
Idempotency, the part that bites in production
Stripe retries webhooks on failure. The same `invoice.payment_succeeded` event can hit your endpoint two or three times in a few minutes. A naive handler that 'just adds the credits' will silently double-credit the customer. SaaSForge AI and Core both track event IDs in a `stripe_events` table and reject duplicates before any state mutation.
Idempotency keys are also used inside the application: each credit debit carries an `idempotency_key` (e.g., `chat:<turn_id>`), so a client retry on a flaky network does not double-debit a single chat turn. The pattern is uniform across the codebase, which is the only way to keep it honest.
export async function POST(req: Request) {
const event = await verifyStripeSignature(req);
const seen = await db.stripeEvents.findUnique({
where: { id: event.id },
});
if (seen) return Response.json({ ok: true });
await db.$transaction(async (tx) => {
await tx.stripeEvents.create({ data: { id: event.id, type: event.type } });
await handleEvent(tx, event);
});
return Response.json({ ok: true });
}Customer portal vs custom billing UI
The Stripe Customer Portal is the boring right answer for plan changes, payment methods, invoices, and cancellation. Both templates wire it up: a single endpoint generates a portal session and redirects the user. You skip building the billing UI Stripe already maintains.
Custom billing UI is reserved for things the portal cannot show: credit balance and consumption history on AI, workspace-scoped billing on Core. The split keeps your custom code small while the portal handles the high-stakes parts (payment method storage, invoice access, dispute prevention).