LenDen and ledger-first accounting for small business
Why LenDen treats the ledger as source of truth—double-entry flows, party balances, and building accounting software that stays honest.
LenDen started from a simple frustration: small business owners track who owes whom in notebooks and chat threads, then try to reconcile that with whatever their accountant exports months later. Spreadsheets bend; invoices lie by omission. I wanted software where the numbers always tie—where deleting a “transaction” does not silently erase history, and where running balances are computed from entries, not cached guesses.
LenDen is ledger-first accounting for lending and informal credit between parties—think suppliers, staff advances, partner contributions—not a full ERP. The product thesis is that if the ledger is correct, every screen is a view.
Ledger-first means append-only truth
In a ledger-first model, business events become journal entries. Payments, charges, write-offs, and opening balances all land as balanced lines against accounts. UI “edits” are compensating entries or controlled reversals, not UPDATE statements on a single amount column.
That choice costs short-term convenience—you cannot drag a cell in a grid and pretend the past never happened—but it buys auditability owners understand when cash disagrees with memory.
-- Simplified: party balance derived from ledger lines, not stored totals
select party_id, sum(amount) as balance
from ledger_lines
join journal_entries on journal_entries.id = ledger_lines.entry_id
where party_id = $1
group by party_id;
Postgres functions and RPCs in LenDen enforce invariants at the database: entries must balance, periods can be locked, and party scopes cannot leak across organizations.
Parties, not just contacts
LenDen centers “parties”—people or businesses you transact with—rather than abstract customers/vendors duplicated across modules. A party ledger shows lifetime inflow and outflow with a running balance that matches the sum of entries. Sorting and search respect how owners think: “Show me everyone I owe” vs “Who owes me.”
Lifetime balance was a deliberate regression fix. Early prototypes cached balances on the party row for speed; edge cases around backdated entries and reversals drifted. Moving balance derivation to the ledger removed an entire class of bugs at the cost of indexed queries—which Supabase and proper migrations handled.
Migration 011_party_ledger_lifetime_balance documented that shift explicitly: balances are always derived, never authoritative on the party row. When accountants ask “which number is truth?” the answer is unambiguous—the sum of posted lines.
Double-entry templates behind simple verbs
Users tap “I lent” or “I received”; the app posts balanced lines to configured accounts. Templates are versioned so a change to default cash accounts does not rewrite history. Power mode exposes the journal lines before confirm for users who want to learn or verify.
// Conceptual: template expands to balanced lines
function postReceive(partyId: string, amount: number, note: string) {
return createEntry({
lines: [
{ account: "cash", amount, partyId },
{ account: "receivable", amount: -amount, partyId },
],
memo: note,
});
}
UX that respects non-accountants
Owners are not trained in debits and credits. The app speaks in “you paid,” “you received,” and “you lent,” mapping to templates that generate balanced entries behind the scenes. Power users can expose account codes later; the default path must not require a accounting degree.
Mobile-first layouts matter because entries happen on shop floors, not desks. Amount inputs use locale-aware formatting; notes capture context (“materials for Job 14”) that accountants will ask about later.
Empty states explain the model: a new party shows zero balance with copy about how the first entry will appear. Without that education, users think the app is broken before they post anything.
Reports owners actually need
Statements by party and date range—who owed what when—matter more than glossy charts early on. PDF or shareable summaries export from the same ledger queries as the UI. If the PDF total disagrees with the app, you do not have ledger-first; you have two sources of truth.
Why Supabase and tested migrations
LenDen uses Supabase for auth, row-level security, and SQL migrations checked into the repo. Balance regressions are scary, so migration files include comments linking to QA scenarios—backdated entries, party merges, deleted drafts that should not affect posted ledgers.
Vitest covers pure calculation helpers; database RPCs get integration tests where possible. The goal is not 100% coverage theater—it is confidence that a migration cannot ship if lifetime balance diverges from line sums.
Row-level security in Supabase keeps each organization’s parties and entries isolated. Client-side checks are never sufficient; policies enforce tenant boundaries even if someone crafts a request by hand.
Building in public as a product studio
LenDen is both a product and proof of how SataniLabs ships: small surface area, hard invariants, documentation beside code. Writing about ledger-first accounting on the studio blog connects the marketing site to the product thesis without turning posts into release notes.
What ledger-first does not solve
LenDen is not payroll, inventory, or tax filing. Ledger-first accounting clarifies obligations between parties; it does not replace a CPA. Roadmap features—statements, reminders, export to Tally/QuickBooks—must still emit from entries, not parallel shadow tables.
Building LenDen reinforced a lesson I bring to client work: pick the invariant you refuse to break, then let UX flex around it. For us, that invariant is the ledger. Everything else is a lens on the same truth.