On this page
ADR-0005: Append-only double-entry ledger with database-enforced balances
Status: Accepted (plan decision D5). Implemented in crates/commonlayer-funding and 0010_funding.sql; the application runs as a least-privilege role since WP-70a (amended 2026-11-10).
Context
CommonLayer holds money on behalf of projects and pays it to maintainers and contributors. "Funds that are not available cannot be spent" and "financial records are never edited" have to hold even if application code has a bug.
Decision
- A double-entry ledger in the style of transfer-based ledgers: accounts, append-only transactions and entries. Every entry moves a positive amount from a debit account to a credit account of the same currency, so the sum of all balances is zero.
ledger_balancesis maintained only by anAFTER INSERTtrigger onledger_entries. The trigger refuses to take an internal account below zero (SQLSTATECL001, mapped tofunding.insufficient_funds) and refuses currency mismatches (CL008). A guard trigger rejects any direct write toledger_balances(CL005).- Ledger tables carry
forbid_mutationtriggers:UPDATE,DELETEandTRUNCATEraiseCL002. Corrections are new transactions. - Every transaction has a unique idempotency key; posting the same key twice returns the existing transaction.
- Only
crates/commonlayer-funding/src/ledger.rsinserts into ledger tables; a test scans the crate's sources to keep it that way.
Consequences
- The invariants are enforced against application bugs and against ordinary SQL clients using the application's role. They do not stop a database superuser or the table owner, who can disable triggers. The platform's migrations currently run as the same role the application uses; see the open items in the threat model.
- Reconciliation (hourly) recomputes balances from entries and checks the zero sum, so drift caused by such out-of-band changes is detected even though it cannot be prevented by the schema alone.
- The full specification is in the ledger specification.
Amendment, 2026-11-10
- The statement above that migrations run as the application's role is no longer true. Since WP-70a the processes connect as a runtime role that owns nothing and cannot disable triggers; migrations run with a separate owner connection (ADR-0036). A superuser or the owner role can still disable triggers, and there is still no hash chain over the ledger.