The boundary
Two layers, one deliberate line between them
The engine guarantees the books are correct. The core decides what the
books mean. A front-end talks to the core in the language of documents —
invoices and bills; the core talks to the engine in the language of balanced
entries. The engine talks to no one — it only enforces.
Core layer
What the books mean
- Chart-of-accounts catalog
- Documents → journal entries (an invoice debits A/R, credits Revenue + Tax; a bill debits an expense, credits A/P)
- Period definition & close orchestration
- The higher-level API front-ends call
POST /api/documents/invoice · /bill
hands down a balanced entry
Engine layer
What must stay true
- Every entry balances: ∑ debits = ∑ credits
- Accounts normalize & roll up by type
- Posted entries are immutable — correct by reversal
- Nothing posts into a locked period
POST /api/journal-entries
Four types — one per distinct (normal balance, statement) pair
| Account type | Normal balance | Statement | Resets at close |
| Asset | Debit | Balance sheet | No — permanent |
| Liability / Equity | Credit | Balance sheet | No — permanent |
| Revenue | Credit | Income statement | Yes — temporary |
| Expense | Debit | Income statement | Yes — temporary |
Liability and equity collapse into one credit-normal type at the engine level — they
behave identically. The debt-vs-equity split lives in the chart, not the type system.
Integrity by construction, not by convention
Double-entry is the only write
Everything that touches money is a balanced entry of two or more lines. The engine never lets an unbalanced entry commit.
Immutable, correct by reversal
Posted entries are never edited or deleted. A mistake is fixed by posting a reversing entry — an audit trail by construction.
Documents, not debits
Front-ends submit invoices and bills. They never compute a debit. The core decides the postings; the engine enforces the balance.
Periods that lock
A posting’s date determines its period. A locked period refuses new entries — corrections become visible back-dated entries, never silent edits.
Sub-ledgers by opaque key
A line can carry a balance key — a customer, a vendor. The engine reconciles each key to its account total without learning what the key means.
Retained earnings lives above
The engine tracks revenue and expense; it does not close them. Closing net income into retained earnings is a core concern.
The full design notes and Prisma schema live in /docs in the repository.
Lending sits above the engine too
The consortium lends its pooled partner capital to small businesses. A
disbursement debits Loans Receivable and credits Cash; the flat
$195 origination fee and any late fees credit Loan Fee Income; and a
payment debits Cash, crediting Loan Interest Income for the
interest portion and Loans Receivable for the rest. Each loan is tracked
as an opaque balance_key sub-ledger of Loans Receivable — the same
mechanism behind per-customer receivables and per-partner equity — so every loan
reconciles to the control account by construction. How each entry is formed is
documented in /docs/loans.md and in netlify/lib/loans.ts.
Members may also fund an individual loan directly. When a funded loan is activated,
the Core posts one balanced allocation entry that shares out the loan's earnings:
each contributor earns 8% on what they put in (credited to their
Partner Investments key), and the remaining interest plus the $195 admin fee
flow to the group-level DHC Joint Pot. When a member leaves, one balanced
exit entry locks in their payout — their capital plus a pro-rata share of that pot —
by moving both into an Owed to Departed Members liability and reducing the
pot accordingly; the member then drops out of all future ownership calculations. All
still ordinary journal entries the engine enforces.
Integrations sit above the engine
Recurring billing (Stripe) and bank feeds (Plaid) are layered above the
engine, never around it. A due recurring schedule generates an invoice through the
same Core boundary, and an automatic payment posts a normal cash receipt — both are
ordinary balanced entries the engine enforces. Imported bank transactions are matched
against existing journal entries during reconciliation; they never rewrite them. Where
each piece of Stripe and Plaid logic lives is documented in
/docs/integrations.md.