Kuatia is an append-only, auditable, multi-asset UTXO-style ledger library in Rust. Value is tracked as signed postings — no mutable balance fields. Transfers atomically consume and create postings, enforcing per-asset conservation (double-entry bookkeeping).
crates/
kuatia-types/ Domain types: AccountId, Posting, Movement, Cent, AutoId, etc.
kuatia-core/ Pure, sync, no-IO logic: validation, hashing, posting selection
kuatia-storage/ Store trait (5 sub-traits), InMemoryStore, conformance tests
kuatia-storage-sql/ SQL backend: SQLite/PostgreSQL via sqlx
kuatia/ Async layer: Ledger resource, saga pipeline, intent API
doc/
architecture.md Architecture decisions and rationale
crates.md Crate reference: modules, types, APIs
accounts.md Account model, policies, lifecycle
transfers.md Transfer/Movement API, resolve algorithm
glossary.md Terms, book design, exchange & supermarket examples
{ from, to, asset, amount } — the fundamental unit of intent. All operations (pay, deposit, withdraw) are one or more movements.sum(consumed) == sum(created).legend crate.Arc<dyn Store>: Ledger is non-generic, enabling concrete saga types.Two-pass:
to and accumulate net debit on from.Deposit: two movements cancel to zero net debit on the system account — no posting selection needed.
cargo test # runs all 119 tests across all crates
cargo test -p kuatia-core # pure core tests only
cargo test -p kuatia # integration + saga tests
SUM, MAX, MIN, AVG, or any computation on monetary amounts or domain values in queries. COUNT(*) for pagination row totals is allowed (it counts rows, not domain values). Balances are always computed in Rust with checked arithmetic (checked_add, checked_sub, checked_neg) — no silent overflowunwrap()/expect() in production code — all errors bubble up via Result.pay(), .deposit(), .withdraw()) over raw .movement() constructionstore_tests! macro — new trait methods require new tests.deposit() returns Result<Self, OverflowError> — callers must handle the errorNo AUTOINCREMENT / SERIAL in the database — all IDs are generated in Rust. Use snowflake-style i64 IDs with the following bit layout:
[0][ 40 bits: ms timestamp ][ 23 bits: CRC32(data) ]
^sign (always 0 = positive)
AutoId in kuatia-types/src/autoid.rs, includes inline CRC32 (IEEE)BIGINT — the DB never assigns IDs