Mastering SaaS billing
Subscriptions, one-time payments and credit systems in a Next.js app, without the webhook horror stories.
Jane Doe · Engineering
Most billing bugs are not billing bugs. They are state-synchronisation bugs wearing a billing costume: Stripe knows one thing, your database knows another, and the customer sees a third.
Treat Stripe as the source of truth
Never write subscription state from a server action. The user clicks upgrade, you
send them to Checkout, and Stripe tells you what happened through a webhook. That
is the only path that writes to your subscription table.
Why it matters
If both the client flow and the webhook can write status, a slow webhook and a fast redirect will race, and the loser wins.
Make handlers idempotent
Stripe retries. It will deliver the same event twice, and your credit grant will run twice unless you give it a key to deduplicate on.
await grantCredits({
organizationId,
amount: plan.monthlyCredits,
reason: 'plan_grant',
idempotencyKey: `invoice:${invoice.id}`,
});A unique index on idempotency_key turns the second insert into a no-op.
Derive balances, don't store them
An append-only ledger costs one SUM per read and buys you a full audit trail,
safe refunds and the ability to answer "why is my balance 340?" without guessing.