Stripe delivers webhooks at least once. Not exactly once — at least once. A network blip on your side, a 200 that arrives a half-second too late, an event Stripe decides to re-send during an outage, and the same checkout.session.completed lands on your endpoint twice. Sometimes three times. Your handler does not know that.
If that handler grants a subscription, sends a welcome email, or increments a usage counter, "at least once" is how one signup becomes two welcome emails and a double-counted seat. The fix is not a queue, not a distributed lock, not an idempotency framework. It's one Postgres table, one unique constraint, and catching one error code: 23505.
I run a Discord-native Company Brain — teams /save docs and /ask grounded answers, billed at a flat monthly price through Stripe. The entire billing surface is one Fastify route and one Supabase Postgres. Here's the whole idempotency story, in the order the bytes actually arrive.
The problem: webhooks are at-least-once, your side effects are not
A Stripe webhook is a state change that already happened — a card was charged, a subscription renewed. Your job is to reconcile your database to that fact. The trap is that Stripe's delivery guarantee (at-least-once) and your handler's behavior (run the side effect every time) don't match. Run a non-idempotent handler twice and you've corrupted state with no error to tell you.






