Kuatia supports journaling. A committed transfer is a journal entry, and the transfer log is the accounting journal: the append-only, ordered book of every committed entry. This page states what that means in practice, shows single, compound, and multi-asset entries, and explains why the log is auditable by replay.
For the vocabulary mapping (journal vs. journal entry vs. Book, and the terms
that are easy to conflate) see accounting-mapping.md.
For the resolve algorithm and the builder API see transfers.md.
For the decision record see
ADR-0013.
In classical bookkeeping:
Kuatia provides the same guarantees over a UTXO-style, posting-based model:
| Classical accounting | Kuatia |
|---|---|
| Journal (book of original entry) | Transfer log (TransferStore of EnvelopeRecords) |
| Journal entry (one balanced event) | Committed Transfer resolved into an Envelope |
| Compound journal entry | Transfer with multiple Movements |
Σ debits = Σ credits |
Per-asset conservation sum(consumed) == sum(created) |
The mechanism differs (signed postings instead of mutable balances), the accounting grain is identical: one committed transfer is one journal entry.
A Transfer is a Vec<Movement> committed atomically. Each Movement is
{ from, to, asset, amount }. On commit, resolve() turns the movements into
an Envelope of concrete postings to consume and create, and validation
enforces per-asset conservation before anything is written. The whole entry
commits or none of it does.
A minimal two-account entry:
let transfer = TransferBuilder::new()
.pay(alice, bob, usd, Cent::from(50))
.build();
This is one journal entry: 50 USD leaves Alice, 50 USD arrives at Bob, and the
resolved envelope balances (sum(consumed) == sum(created) for USD).
A compound journal entry is native, not a special case. Because a Transfer
holds a list of movements, chaining builder calls accumulates legs into one
atomic entry.
Classical compound entry (a cash sale with tax):
2026-06-26 Cash sale of goods
Dr Cash ................. 115
Cr Revenue .......... 100
Cr Sales tax payable . 15
The equivalent business effects as one Kuatia transfer:
let transfer = TransferBuilder::new()
.book(sales_book)
.pay(customer, revenue, usd, Cent::from(100))
.pay(customer, tax_payable, usd, Cent::from(15))
.build();
// One Transfer -> one Envelope -> one EnvelopeRecord in the transfer log.
Both are a single balanced event. In the classical entry Σ Dr (115) = Σ Cr
(115). In Kuatia the resolved envelope satisfies per-asset conservation for
USD. The resolve step aggregates net debit per (account, asset) before
selecting postings, so several legs debiting the same account share one
selection pass. See
transfers.md for the aggregation detail, and
the note in accounting-mapping.md
on why this shows business effects rather than a literal debit/credit
translation.
Conservation is enforced per asset, and each asset is an independent conservation boundary. A single entry can therefore move more than one asset as long as every asset balances on its own. This is how a currency exchange or FX trade is recorded as one journal entry: the USD legs balance against USD, and the EUR legs balance against EUR, within the same envelope.
A hand-built multi-asset envelope is committed through the same path with
ledger.commit_envelope(envelope). Validation returns
ConservationViolation { asset, consumed_sum, created_sum } if any single asset
fails to balance.
One entry and the journal differ only in grain:
Transfer / Envelope is one record: one journal entry.TransferStore persists each committed envelope as EnvelopeRecord {
envelope, receipt, created_at } in append-only order.Committing an entry returns a Receipt { transfer_id } naming the committed
envelope. The EnvelopeId is content-addressed (the double-SHA-256 of the
canonical envelope bytes), which gives idempotency (re-committing the same
envelope returns the cached receipt) and tamper evidence (any edit changes the
id).
Kuatia keeps a second append-only sequence, the event log (EventStore of
LedgerEvent), for lifecycle notifications and projections. It is not the
journal. It records that something happened; the transfer log records exactly
which postings moved. See
ADR-0010.
There are no stored balances to drift. An account's balance is the sum of its
Active postings, computed on demand. Re-applying every EnvelopeRecord in
order reconstructs all balances exactly. Because each entry is balanced per
asset and the log is append-only and content-addressed, the journal is both
verifiable (recompute any balance) and tamper-evident (any change to a past
entry breaks its id).
Two Kuatia concepts are easy to mistake for the journal and are not:
Book is a transfer policy scope. It gates which accounts and assets may
participate in a transfer. It is not the journal, not a journal entry, and not
a balance partition. See
accounting-mapping.md.Movement, the resolve algorithm, and the
TransferBuilder API.