Skip to content

fix(credits): make credit deduction an atomic conditional decrement (#71) - #74

Merged
MistryVishwa merged 1 commit into
MistryVishwa:mainfrom
Rudra-clrscr:fix/71-atomic-deduct-credit
Jul 6, 2026
Merged

fix(credits): make credit deduction an atomic conditional decrement (#71)#74
MistryVishwa merged 1 commit into
MistryVishwa:mainfrom
Rudra-clrscr:fix/71-atomic-deduct-credit

Conversation

@Rudra-clrscr

Copy link
Copy Markdown
Contributor

Closes #71

Problem

deductCredit() in lib/database.ts performed a read-then-write:

  1. read the current value via getCredits(userId)
  2. write back remaining - 1 in a separate query

Two concurrent requests for the same user could both read the same value
(e.g. 1) before either wrote, and both compute and write 0 — funding two
operations 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 — adds
    deduct_credit(p_user_id UUID, p_feature TEXT), which does a conditional
    UPDATE ... WHERE <col> > 0 for the feature's _remaining column and returns
    whether a row was affected.
  • lib/database.tsdeductCredit() now calls the function via
    admin.rpc("deduct_credit", ...) and keeps its Promise<boolean> return
    shape, 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 0 and the second re-checks the WHERE against the
updated row, matches nothing (ROW_COUNT = 0), and returns FALSE. That maps
to consumeCredit() returning { allowed: false, reason: "no_credits" }.

Acceptance criteria

  • deductCredit performs an atomic, conditional decrement
  • Concurrent calls with 1 credit result in exactly one success and one rejection
  • Existing callers keep a compatible return shape (still boolean)

Deployment note

The migration must be applied before/with deploy. Supabase automatically
reloads the PostgREST schema so rpc("deduct_credit") is exposed. Until the
function exists the RPC errors and deductCredit returns false — i.e. it
safely denies rather than over-grants.

Testing

  • tsc --noEmit: clean around the change.
  • Not run live: the race behavior can only be exercised against a Supabase DB
    with the function applied. Suggested manual check — set a user's
    ai_chat_remaining to 1, fire two concurrent consumeCredit() calls, and
    confirm exactly one succeeds and the row ends at 0.

…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.
@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

@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 MistryVishwa added bug Something isn't working good first issue Good for newcomers ssoc26 Main tag identifying the repository for Social Summer of Code 2026 Medium 30 Points labels Jul 6, 2026
@MistryVishwa
MistryVishwa merged commit d52dffa into MistryVishwa:main Jul 6, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working good first issue Good for newcomers Medium 30 Points ssoc26 Main tag identifying the repository for Social Summer of Code 2026

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Credit deduction (deductCredit) has a read-then-write race condition

2 participants