fix(webhooks): replace non-atomic SELECT+check with atomic UPDATE claim to eliminate duplicate fulfillments#368
Conversation
…rent retries
All four payment webhook handlers (Stripe, Cashfree, NOWPayments, AbacatePay) used a non-atomic SELECT-then-UPDATE pattern. Two concurrent retry requests could both read status='pending' and both proceed to fulfillItemPurchase, causing duplicate item grants, corrupted consumable quantities, duplicate activity feed inserts, and duplicate notifications/emails.
Fix: replace the SELECT+check with a conditional UPDATE that transitions pending → processing in a single round-trip. Supabase only returns a row to the request that wins the update; the loser gets null and breaks early. fulfillItemPurchase and all downstream side-effects are only reached by the winner.
Sky ads paths are unaffected — they already use .eq('active', false) as an implicit atomic guard on their UPDATE.
|
@Srejoye is attempting to deploy a commit to the ixotic27-8245's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
Hi @Srejoye! 👋 I've reviewed your PR code for resolving concurrent webhook double-fulfillment (Issue #377): Your atomic claim pattern using If two identical webhook requests arrive concurrently, the first request will successfully claim the row and transition it to Please address this race condition so we can review the PR again! Thank you! |
…Cashfree/NOW/AbacatePay
a69fdae to
5c11ce0
Compare
|
Hi @Srejoye! 👋 I've reviewed your PR code for resolving issue #361. It looks like this PR has a regression and conflict:
The atomic claim fixes are still very valuable for AbacatePay, Cashfree, and NOWPayments! Please refactor the PR to preserve the Stripe webhook implementation currently on Thank you! |
8b3b25c to
5089dde
Compare
There was a problem hiding this comment.
🔍 Security Scan: Review Needed
The following patterns were detected in the latest changes:
if (!process.env.ABACATEPAY_WEBHOOK_SECRET) {
if (!process.env.ABACATEPAY_WEBHOOK_SECRET) {
A maintainer should review these findings before merging.
Hey @Srejoye, please review these flagged items! 🛠️
|
🚨 Hey @Srejoye, the CI Pipeline is failing on this PR and it has been marked as 🔍 What failed:
📋 Error Details (first 2):
Please fix the issues before this can be reviewed. Here's how: 1. Run checks locally before pushing: npm run lint # Run ESLint
npm run build # Verify production build passes2. Auto-fix common issues: npm run lint -- --fix # Auto-fix lint errors where possible3. Check the full failure log here: Once you push a fix and the CI passes, the |
What does this PR do?
Fixes a race condition present in all four payment webhook handlers (Stripe, Cashfree, NOWPayments, AbacatePay). Concurrent webhook retries — which all providers send — could both read
status = "pending"before either had written"completed", causing both to callfulfillItemPurchase. This led to duplicate item grants, corrupted consumable quantities, duplicateactivity_feedinserts, and duplicate Discord notifications and emails.The fix is the same single pattern applied consistently across all four files:
Instead of SELECT (read status) → check → fulfill → UPDATE, the handler now does a conditional UPDATE first:
Supabase (Postgres
UPDATE ... WHERE ... RETURNING) is atomic at the row level — exactly one concurrent caller gets the row back; all others getnulland exit immediately.fulfillItemPurchase,autoEquipIfSolo, feed inserts, and all notifications are only reached by the single winner.Sky ads paths are untouched — they already use
.eq("active", false)as an implicit atomic guard on theirUPDATE, which is the same pattern.Files changed:
stripe/route.ts,cashfree/route.ts,nowpayments/route.ts,abacatepay/route.ts— fulfillment block only in each, no changes to auth, parsing, or expiry/refund handling.Related issue
Fixes #361
Checklist