fix(billing): serialize Gmail inbox allowance decisions so racing alias signups can't both keep it

Merged
WA
1d ago
+227-85

Summary

  • Better Auth commits each user row before user.create.after runs, so two signups on one Gmail inbox could race: the later account's earlier-account check could run before the earlier row was visible, and both kept the free allowance (follow-up to #9418)
  • handleNewUser now decides inside one transaction under a per-inbox advisory lock (acquireGmailInboxLock, existing acquireAdvisoryXactLock), inserts the stats row, then withdraws the allowance from any later alias on the inbox that got in first (laterGmailAliasesWithAllowance) — the earliest account keeps it whichever hook runs first
  • The stats-row fallback keeps an unlocked check: it only runs when a signup's own decision failed, and locking there would hold the inbox lock through the caller's transaction; it never withdraws from later accounts, so long-standing aliases are never stripped retroactively
  • identityHadFreeAllowance moves to lib/users/free-allowance.ts; the admin balance grant now uses it, so a stats row it creates applies the Gmail rule too (it previously checked deleted identities only)
  • Alias lookups are a single self-join on user_gmail_mailbox_idx; test schema mock gains gmailMailbox

Type of Change

  • Bug fix

Testing

  • lib/users/gmail-alias.integration.ts on real Postgres (INTEGRATION_DB_PROVISION=migrate): 11/11. New guards shown red with the fix disabled: an alias whose signup decided first loses the allowance once the earlier account decides; a held inbox lock blocks a same-inbox signup (pg_blocking_pids); the fallback never withdraws from a later alias. Index test now EXPLAINs the self-join shape
  • lib/billing/core/usage.test.ts, lib/admin/dashboard-credit-grant.test.ts: 30/30
  • bun run lint, check:file-names, check:test-patterns, check:comment-hygiene, check:unused-exports, check:boundaries, check:schema-mock; drizzle-kit generate reports no drift
  • Full type-check / audits / suites left to CI

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing (new tests pass the test-audit authoring gate)
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)