EngineeringCredits
Designing an append-only credit ledger
May 26, 2026
If credits are your currency, the ledger is your bank. Here is the shape we landed on after a few painful rewrites.
The contract
The database schema is the contract. Balances are derived, the ledger is authoritative:
credit_transaction— append-only, one row per movement (grant, spend, refund).user.credits— a cached balance, only ever changed by a guardedUPDATE.
Idempotent fulfillment
Stripe retries webhooks. Keying every grant on the Stripe object ID makes replays free:
// Upsert on the event id — a retried webhook is a no-op.
await db.insert(creditTransaction)
.values(entry)
.onConflictDoNothing({ target: creditTransaction.stripeEventId });
Rules we don't break
- Never
SET credits = <computed in JS>. - All Stripe state changes flow through webhooks — success pages are UI only.
- Every spend is metered by real token usage, never an estimate.
Get these three right and your billing simply stops being a source of bugs.