Design Principles
Each section of these docs describes a different subsystem, but the same handful of patterns recur in all of them. If you internalize these, most of the codebase’s “why is it built this way?” questions answer themselves — and most review feedback you’ll receive is one of these principles being defended.
Never two authored truths for one money fact
The oldest and most-repeated rule (ADR-0011 calls the violation the “dual identity smell”). Every money fact has exactly one canonical, authored home; everything else is a projection with recorded provenance. PCAP per-LP component balances are a projection over the GL, never a second ledger (ADR-0010/0017/0034). GP-ness is derived from legal_entity_category, not stored as a partner role. fund_terms_index is a derived projection of the terms payload, and engines are mechanically banned from reading it (ADR-0019). recordEntityRelationship outright refuses to author an edge that is derivable from a partner_account chain. When you find yourself adding a second table that restates a fact, stop — add a projection with source_ref/source_hash instead.
The determinism spine
Anything that participates in fund accounting must be replayable to the byte, because the audit story rests on “same inputs, same output” being a mechanically checkable claim rather than a promise:
Concretely:
- Canonical-JSON hashing everywhere:
application_hash,payload_hash, postinginput_hash/output_hash, plan hashes — all computed over@opsdna/canonical-jsonencodings. - Deterministic ids: the Lab-rebuilt entity spine is uuid v5 over immutable stable keys; ledger accounts derive from
canonical:<kind>stable keys; journal entries and allocations are content-hash ids. Surrogate uuid v7 is reserved for operational/append tables. See the identifier policy. - The freeze set: ids that feed hashes (
tenantId,accounting_entity_id,book_id,*SourceEventId, …) are frozen by CI lint — change one and every downstream hash changes. - Byte-identical rebuild as the proof: the Acceptance Lab replays whole fund scenarios and diffs the output; pinned engine versions (
LEDGER_CODE_VERSION, schema/hash version constants) make “same inputs, same bytes” a testable claim.
Application / Trace / Row, dispatched on kind
Fund-economics runtime emits <Domain>Application canonical records and <Domain>ApplicationTrace bundles; persistence owns narrowed <Domain>ApplicationRow projections with adapter pairs proving forward and reverse identity (ADR-0001/0002). The three surfaces live on different sides of the package boundary, enforced by scripts/check-package-boundaries.ts and boundary tests. Generic consumers dispatch on the kind literal of a discriminated union with never-default exhaustiveness — there is deliberately no shared supertype, so adding a domain forces every consumer to decide what to do with it. Diagnostics are closed per-domain unions; a stringly code: string field is banned. See Application Records.
Deterministic-id idempotency for replay safety
Side-effectful pipelines never rely on “hopefully this runs once.” They derive the output’s identity from the input, so replays converge: a bank transaction version promotes to exactly one operating source event id:
// the OSE id is a pure function of the transaction version — replays converge
const operatingSourceEventId = `ose:bank:${version.id}`saveCandidate answers already_saved, SQS redeliveries and re-driven promotions are no-ops, and follow-on dispatch records are idempotent via deterministic proof-link identity. If you’re writing a worker, your first design question is “what is the deterministic id of the thing I produce?” See Plaid Sync.
Fail closed, everywhere
Ambiguity is an error, not a guess. Tenant resolution with zero grants yields no_workspace; with two-plus grants it requires an explicit, grant-verified selection header (ADR-0033). resolveFundEntityRouting errors on multiple ManCos or GPs rather than picking one. Terms profiles reject shapes they don’t support (unsupported_for_profile) and executability is a firewall, not a warning. Unregistered source-claim types are rejected. An ambiguous malware-scan verdict leaves the document unviewable. Branded route refs reject wrong-kind ids at decode, before any DB read. Default-deny is the portal’s login posture. The pattern: enumerate the outcomes, and make “I’m not sure” a typed failure.
One LLM trust boundary
LLM outputs are constrained behind Effect/Zod schemas, registries, validators, and evals — and the runtime narrows model authority to a single seam: the query planner’s candidate extraction. The model proposes untrusted concept-resolution candidates; a deterministic resolver, a finite 14-operation algebra, plan validation, and the permission engine do everything after that. Planners cannot invent operations; domain growth happens through registries. Extraction pipelines follow the same shape: micro-extraction per clause with span-cited evidence, deterministic assembly in tested TypeScript — the model never composes the tree. Every LLM-facing surface has an eval suite (bun run evals:all). See AI & Evals and Query Runtime.
Money is a branded bigint, never a number
packages/ledger/src/money.ts defines Money as a branded bigint at scale 4 (ratios/quantities at scale 12):
// packages/ledger/src/money.ts
declare const MoneyBrand: unique symbol
declare const RatioBrand: unique symbol
export type Money = bigint & { readonly [MoneyBrand]: void }
export type Ratio = bigint & { readonly [RatioBrand]: void } // scale 12There are no IEEE floats anywhere on a money path: multiplication rounds half-even from a scale-16 intermediate, and Money.divToParts does largest-remainder allocation whose parts sum exactly to the total. Arithmetic traces carry canonical-decimal projections so a re-hash proves equivalence. If you see amount: number in new code, it’s a bug by type construction. See Ledger Kernel.
The edge is stateless; the database is the boundary
The portal Worker holds no DATABASE_URL and enforces only UX-grade gates; the real authorization boundary is apps/api (JWT verification + tenant resolution) backed by Postgres FORCE RLS on tenant_id, with writes to sensitive tables funneled through SECURITY DEFINER functions owned by narrow resolver roles. Client-forwarded context (like the active-tenant header) is treated as an untrusted selection that must land inside a server-verified grant set. Two independent walls, so a bug in one is not a breach. See Portal Auth & Tenancy and Roles & RLS.
The classic ways to violate these principles by accident: storing a derivable fact in a second table instead of a projection (dual identity smell); putting a number on a money path; giving a worker output a random id so replays duplicate; letting an ambiguous case fall through to a default instead of a typed failure; and letting LLM output touch anything that isn’t schema-validated. Reviewers look for exactly these.
Corollaries you’ll meet in review
- Persist material state transitions: audit events, trace bundles, outbox rows — if runtime behavior matters, it leaves a record.
- Supersede, never erase: version chains (bank transactions, OSEs, ledger source events, terms versions) clip and point at replacements; posted facts are immutable.
- Forward-only migrations: applied SQL is checksum-frozen; the active chain lives in
post-baseline/. See Migrations. - Prefer the stronger path: CLAUDE.md’s standing instruction — don’t relax invariants or take brittle shortcuts to move faster; unwinding a weak shortcut costs more than doing it right.