Core Concepts

The Ledger

Every financial event is a permanent entry. The ledger is append-only, enforced by the database, and corrections are new entries rather than edits.

An entry is an event

A ledger in OpenBooks is a single ordered sequence of events belonging to one organization. Each event records something that happened: a contribution arrived, an expenditure was classified, a filing was drafted, a policy was activated, a member was given a role.

The unit is deliberately an event rather than a balance. A balance is a conclusion, and conclusions can be recomputed from events. Events cannot be recovered from conclusions. Storing the events and deriving everything else means the derived views can be rebuilt, corrected, or extended later without rewriting history — which is the one thing this system does not permit.

Entries are numbered per organization with a sequence number, seq, starting at zero. Sequence numbers are unique within an organization and have no gaps.

Append-only is enforced, not promised

“Append-only” is a claim many systems make and few can substantiate, because in most of them it is a convention: the application declines to issue updates, and the property holds exactly as long as every future code path remembers to. That is a policy, not a guarantee.

Here it is enforced in the database. Triggers on the ledger table intercept every UPDATE and every DELETE and raise an exception unconditionally:

ledger_events is append-only; UPDATE/DELETE not permitted

The rule is enforced beneath the application, so it holds regardless of what the application asks for. A bug, a bad migration, or a careless script cannot quietly revise an entry; the write fails. This matters because the hash chain in The Hash Chain makes tampering detectable, while the trigger makes the ordinary path of tampering impossible. Detection and prevention are different defenses and this ledger uses both.

What an entry contains

tenant_id

The organization the entry belongs to. Ledgers do not interleave.

seq

Position in this organization's sequence, starting at zero.

event_type

What happened — one of the types listed below.

content

The event's payload, as structured JSON. Its shape depends on the event type.

actor_id / actor_email

Who caused the entry. An entry always names a responsible party.

created_at

When the entry was written.

prior_hash

The hash of the entry immediately before this one. This is the link.

event_hash

The SHA-256 of this entry's canonical form, computed over every field above.

Because prior_hash and actor_email are inside the hashed material, neither the ordering of the ledger nor the attribution of an entry can be changed without the change being detectable.

Event types

The vocabulary is fixed. An entry is one of these, and nothing else can be appended:

  • Organizationtenant.genesis, tenant.member.added, tenant.member.removed, tenant.member.role_changed, tenant.settings.updated
  • Money in and outcontribution.created, contribution.classified, contribution.voided, expenditure.created, expenditure.classified, expenditure.voided
  • Campaignscampaign.created, campaign.updated
  • Filingsfiling.drafted, filing.submitted, filing.accepted, filing.rejected
  • Classificationclassification.applied, classification.overridden
  • Policiespolicy.created, policy.activated, policy.archived, policy.violation.resolved
  • Banking and auditbank.connected, bank.disconnected, audit.export

Every ledger opens with tenant.genesis at seq 0, whose prior_hash is sixty-four zeros because there is nothing before it.

Corrections

Errors are ordinary. A vendor is miscategorized; an amount is entered wrong; a classification that looked right in March looks wrong in June. A ledger that could not absorb error would be a ledger nobody could use honestly.

A correction is a new entry. classification.overridden records that a human replaced an automated classification; contribution.voided and expenditure.voided record that an item should no longer count. In each case the original entry stays exactly where it was, with its original hash, and the correcting entry refers back to it.

The consequence is worth stating plainly, because it is the point: the ledger does not show you a clean record. It shows you the real one, including the parts the organization would rather not display. An outside reader can see not only what an organization concluded but when it changed its mind, and how often. That is a feature of the design and not a limitation of it.

There are no entry states

It is tempting to describe a ledger entry as moving through states — drafted, then pending, then published, then perhaps corrected. That model does not apply here, and describing it as if it did would misrepresent the system.

There is no status field on an entry, because an append-only ledger does not need one. An entry that exists has been appended, and appending is the only thing that ever happens to it. There is no unpublished state to leave and no published state to enter; there is no “republish,” because republication would mean rewriting, and rewriting is what the triggers forbid. Lifecycle, where it exists, belongs to the things the ledger describes — a filing moves from drafted to submitted (see Filings) — and that movement is itself recorded as further entries.