Thank you! And honestly — re-fetch alone WAS enough for state correctness, and that's
the part I'd emphasize to anyone pre-payments: if every handler re-fetches live state
by subscription ID instead of trusting the payload, processing becomes convergent.
A duplicate event just re-fetches the same truth and writes the same row. Duplicates
become harmless no-ops against the DB.
Where it still bites is side effects. The DB converges, but nothing stops a duplicate
from re-sending the "Welcome to Pro" email, double-logging an upgrade analytics event,
or burning a redundant Stripe API call. So no — I didn't originally store event IDs,
and I learned the hard way that state idempotency ≠side-effect idempotency.
What's on the backlog now: a small processed_events table (event_id PK, type,
created_at, processed_at). Handler checks it first and returns 200 early on
duplicates, plus an ordering guard — only apply if event.created is newer than the
last applied timestamp for that subscription, so a stale retry can't overwrite
fresher state under concurrent delivery.
Since you're pre-payments on Supabase, the combo I'd wire from day one:
(1) verify signatures, (2) re-fetch live state, (3) event-ID store for side effects.
It's maybe 30 lines of Edge Function and it deletes an entire class of 2 AM bugs.
Happy to share my handler skeleton if it's useful — good luck with the wiring,
it's about to get interesting!
The zombie-user bug is a great catch. Trusting webhook order feels reasonable right up until a retry lands late, and re-fetching the live subscription by ID is the kind of fix that’s only obvious after you’ve been burned. I’m a solo dev on Supabase too and haven’t wired payments into my product yet, so this goes straight into my notes. Did you also store processed event IDs to skip duplicates, or was the re-fetch enough on its own?