Credit accounting model
Bursar records monetary state in one tenant-isolated PostgreSQL schema. This page explains the accounting entities and invariants; use the database schema and SDK references for exact tables and method signatures.
Accounts
A subject (typically one user) has one personal credit_accounts row, and
optionally one account_kind = 'team' row per team. Each account carries a
single locked canonical balance and a version that every mutation checks —
the hot path never re-derives balance from history.
The append-only ledger
Every monetary event is one row in credit_ledger_entries: a signed amount,
the exact balance_after after posting, an idempotency key, and a
reference_entry_id for reversals. Kinds are grant, purchase, usage,
expiry, revocation, refund, adjustment, reservation, release, and
refund_clawback. Positive kinds post credits; usage, expiry, and
reservation post debits; adjustment can go either way. A refund reverses
its source: the refund entry restores the spent balance, and a
refund_clawback entry reverses the original debits.
Lots and allocations
Every positive entry becomes a credit_lots row: the amount, its bucket,
priority, and expiry. Debits consume lots in priority order — bucket tiering
is credit_lot_allocations rows fanning out of the debit entry. When credits
expire or are revoked, the remaining lot balance is allocated as expiry or
revocation and the account balance moves by exactly that amount. Refunds
restore the source lots that the original debit consumed, not the account
globally, so expiry semantics survive refunds.
Leases
credit_leases are the atomic admission gate for long-running work:
reserve captures a worst-case hold, a policy snapshot, and the
minimum_balance at reservation; settle charges the actual cost and
finalizes; release returns the hold. Entitlement, quota, and
max_in_flight checks all happen in the same transaction as the reserve, so
nothing drifts between "can I afford this?" and "you are billed".
Plans as state
Plans are non-monetary. account_plan_assignments records which plan an
account holds against which catalog revision; allowance_windows and
quota_windows track usage windows and consumption keyed by account. The
allowance is real money available to the account, but it never enters the
balance — DeductionResult.allowance_consumed tells you the split.
Usage charges
Each metered charge writes one compact credit_usage_charges row: operation,
measures, model, requested, charged, allowance_requested,
allowance_covered, and the pricing snapshot. The charged + allowance_covered = requested check is part of the schema. Zero-cost usage is still recorded,
so quotas and analytics cannot be bypassed by a free rate.
Core tables
| Table | Purpose |
|---|---|
credit_accounts | One locked balance and version per account (personal or team) |
credit_ledger_entries | Append-only money history with balance_after per entry |
credit_lots | Each positive entry as a spendable lot with bucket, priority, expiry |
credit_lot_allocations | Which lots a debit consumed, in priority order |
credit_leases | Temporary holds and policy snapshots for admission control |
account_plan_assignments | Current plan per account, pinned to a catalog revision |
The three money invariants
- One locked balance per account.
credit_accounts.balanceis written only by the ledger-posting path, guarded byversion; nothing mutates it directly. - Balance equals the ledger. Every mutation appends an entry whose
balance_afterequals the resulting account balance — reading history and reading the balance always agree. - The ledger is append-only and idempotent. Entries are never updated or
deleted, and
(account_id, idempotency_key)is unique, so a retried write replays the original entry instead of double-posting.
Transaction boundary
Every monetary mutation runs in one store transaction that locks the account before it checks idempotency, policy, and available value. Concurrent deductions therefore cannot authorize against the same balance snapshot, and application code never maintains a second financial counter.
Balances and availability
The account balance is the canonical posted value. Availability subtracts active lease holds, and bucket balances divide spendable credits by priority and expiry policy. Availability reads are suitable for display, but only an atomic deduction or reservation can authorize work under concurrency.
Use the credit lifecycle guide for balance and ledger procedures. Use the Python or TypeScript API reference for exact query signatures.
Related
- Architecture: the facade and how capabilities map to the schema
- Configuration and catalog revisions: the document that defines buckets, lots, and plans
- Protect monetary operations: holds, leases, and the money invariants in action
- Credit lifecycle tutorial: every entry kind on one ledger