Payment providers deliver webhooks at least once. That is not a bug in their
systems, it is the only guarantee a network can offer: if your endpoin...
For further actions, you may consider blocking this person and/or reporting abuse
i’m 12 and building an ai mentor on edge compute, so i’m not processing saas billing yet. but reading "no window in which the event is marked done but the business write is missing" just clicked something massive in my brain. 🤯
i use supabase + cloudflare workers for koda's backend. when i eventually add subscriptions, i was planning to just use supabase edge functions to catch the webhook and update the user row. but your point about the "check then process" race condition means i need to wrap the
INSERT INTO processed_eventsand theUPDATE profilesin a single supabase rpc transaction with a unique constraint on the stripe event_id.no background queue needed, just pure db-level atomicity on the edge.
thanks for the masterclass, benjamin. saving this for when koda gets its first paying user. 🐯
That's exactly the right takeaway, Harun. One RPC that inserts the event ID (with a unique constraint) and updates the profile in the same transaction means a retry either does nothing or does everything. One more tip for later: in that function, re-fetch the subscription from Stripe by ID instead of trusting the payload. Good luck with Koda, and congrats on that first paying user when it comes. 🐯
benjamin, that is a pro-tier security tip.
re-fetching the subscription state directly from stripe by id, rather than trusting the webhook payload blindly, completely eliminates the risk of payload tampering or replay attacks. it’s a small extra rpc call that buys massive peace of mind.
adding that exact pattern to the koda roadmap immediately. thanks for the validation and the warm wishes! 🐯💳
"No window in which the event is marked done but the business write is missing" is the clearest statement of that pattern I've seen.
Two additions. The tolerance window and the dedupe table defend different threats , replay attacks versus the provider's own retries , and neither substitutes for the other. And ordering is the gap this doesn't close: Stripe doesn't guarantee delivery order, so if your logic branches on the transition rather than the state, you need to compare versions and discard the stale one.
processed_events also needs retention that exceeds the provider's maximum retry horizon.
Both good points. Agreed that signature tolerance and dedupe solve different problems. On ordering: that's one reason I re-fetch the object by ID. If the handler reacts to the current state rather than to the transition, an out-of-order event just reads the latest state and becomes a no-op. When you really do need to act on the transition, comparing versions as you describe is the way.
The re-fetch-by-ID rule is the one I would underline — I'm Chris, founder of SignalEDI, and trusting the payload is how a replayed EDI shipment notice resurrects a cancelled order days later, so we treat the inbound document as a pointer and re-read the authoritative order state before writing. Pairing the primary-key dedup in the same transaction with keeping processed events past the provider's retry window also closes the crash-between-steps window we have been bitten by. How long do you actually retain processed events before archiving, given dispute events can arrive months after checkout?
Good question. Disputes arrive as new events with their own IDs, so the dedupe table only has to outlive redeliveries of the same event. Stripe retries for up to 3 days and only keeps events retrievable for 30 days, so anything older can't come back. I keep [N days / everything] in processed_events [and archive after that]. Long-lived disputes are handled by re-fetching the charge, not by the dedupe table.