fix(credits): make credit deduction an atomic conditional decrement (#71) - #74
Merged
MistryVishwa merged 1 commit intoJul 6, 2026
Conversation
…istryVishwa#71) deductCredit() read the current remaining value and then wrote remaining - 1 in a separate query. Two concurrent requests for the same user could both read the same value (e.g. 1) and each write 0, letting a single credit fund two operations. Move the decrement into a deduct_credit() SQL function that does a conditional UPDATE (... WHERE <col> > 0). Under row-level locking the loser of a race on the last credit re-checks the guard, matches no rows, and returns FALSE, so concurrent calls resolve to exactly one success and one no_credits rejection. deductCredit() now calls the function via rpc() and keeps its boolean return shape, so consumeCredit() and other callers are unchanged.
|
@Rudra-clrscr is attempting to deploy a commit to the vishwamistrylearning-1037's projects Team on Vercel. A member of the Team first needs to authorize it. |
MistryVishwa
approved these changes
Jul 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #71
Problem
deductCredit()inlib/database.tsperformed a read-then-write:getCredits(userId)remaining - 1in a separate queryTwo concurrent requests for the same user could both read the same value
(e.g.
1) before either wrote, and both compute and write0— funding twooperations from a single credit.
Fix
Move the decrement into a Postgres function so it happens in one atomic,
conditional statement.
supabase/migration-atomic-deduct-credit.sql— addsdeduct_credit(p_user_id UUID, p_feature TEXT), which does a conditionalUPDATE ... WHERE <col> > 0for the feature's_remainingcolumn and returnswhether a row was affected.
lib/database.ts—deductCredit()now calls the function viaadmin.rpc("deduct_credit", ...)and keeps itsPromise<boolean>returnshape, so
consumeCredit()and every other caller is unchanged.Under row-level locking, when two calls race on the last credit the first
commits the decrement to
0and the second re-checks theWHEREagainst theupdated row, matches nothing (
ROW_COUNT = 0), and returnsFALSE. That mapsto
consumeCredit()returning{ allowed: false, reason: "no_credits" }.Acceptance criteria
deductCreditperforms an atomic, conditional decrementboolean)Deployment note
The migration must be applied before/with deploy. Supabase automatically
reloads the PostgREST schema so
rpc("deduct_credit")is exposed. Until thefunction exists the RPC errors and
deductCreditreturnsfalse— i.e. itsafely denies rather than over-grants.
Testing
tsc --noEmit: clean around the change.with the function applied. Suggested manual check — set a user's
ai_chat_remainingto1, fire two concurrentconsumeCredit()calls, andconfirm exactly one succeeds and the row ends at
0.