Statutory numbering and period close stay upstream, where the auditors already look. Ledger is the copy you can ask questions of, without touching the one you cannot.
Every finance team has lived some version of this.
ACDOCA has the answer. Getting it out means a transport, a functional consultant, and a two-week queue.
Finance asks a question, IT writes a report, the report is wrong, repeat. The data was never the bottleneck.
A CSV dump loses the tree, loses the double-entry, and arrives as one wide table nobody can reconcile.
A trial balance you can only see on the fifth working day is not a control. It is a postmortem.
The spreadsheet, the BI extract, the deck — three numbers for the same month, none of them traceable back.
Once the data leaves the ERP, no invariant follows it. Unbalanced rows are found by eye, or not at all.
The ERP is not wrong. It is just not answering.
A transaction is a dated fact. Its entries are how it lands on accounts. The entries' debits equal their credits. Everything else follows from that sentence.
Five types, a parent tree, and nothing cached. Normal balance and statement placement are derived from type, never stored — so no two rows can disagree.
One business fact — date, narration, status, and source_ref back to the upstream document. It carries no amount of its own.
One account's share of a transaction. Unsigned debit and credit in minor units, exactly one non-zero, summing to equality across the transaction.
Postgres can say “this row is valid” but not “these rows sum correctly.” So the invariants live on the write path, and every caller goes through them.
Enforced when a transaction is posted, in one database transaction, all-or-nothing. There is no path that writes a half-balanced fact.
A heading is a rollup, not a bucket. Post to the leaves; the tree adds itself up.
draft → posted → void. A posted transaction is corrected by another transaction, the way books actually work.
A refused write returns total_debit, total_credit and the difference — usually the missing line — not an adjective.
Most writes come from importers and agents. Most reads come from people looking at reports. Both are first-class.
The full tree — groups and leaves, five types, rolled-up totals that never double-count.
Trial balance, P&L, balance sheet, cash flow. Computed from entries, on demand, at any date.
Accounts, transactions and entries under /api/v1/. Composite responses — one question, one request.
The fob-lgr CLI for terminal-first work. Scripts, ad-hoc queries, sync pipelines.
Idempotent on (source, source_ref). An interrupted sync is safe to re-run — it will not duplicate.
Every doc page is served as raw markdown at /docs/md/. Point Claude at it and it builds correct requests.
SAP keeps the book of record. Ledger keeps a correct, queryable double-entry copy — and everything downstream reads from the copy instead of another one-off extract.