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 guarded UPDATE.

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

  1. Never SET credits = <computed in JS>.
  2. All Stripe state changes flow through webhooks — success pages are UI only.
  3. 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.