- Why keep a private ledger
- The capability model
- The importer is the whole problem
- Rules: classification at scale
- The queries that actually matter
- A note on privacy and backups
- Takeaways
Why keep a private ledger
The useful distinction is ownership of the records and the workflow. A private ledger can keep imports, categorisation, reconciliation, and exports under the operator’s control. That also means taking responsibility for data quality and recovery.
Self-hosting a personal-finance ledger is the smallest possible privacy commitment you can make that still gives you the useful surface: a place to see every dollar, categorize it, and answer questions like “did I actually spend more on coffee this quarter than last?” without anyone else getting to read the answer.
Your transaction history is the most accurate diary of your life that exists. Decide who reads it. — Operating principle, finance domain
The capability model
The runtime is a private double-entry ledger with an import layer, a rules engine, a transaction store, and a review surface. The public page can describe the workflow without listing software, database shape, account count, or importer routes.
- Ledger renders dashboards, accepts transactions, and applies classification rules.
- Importer ingests bank exports and normalizes them into clean rows.
- Transaction store keeps the durable history inside the trust boundary.
- Cache keeps the UI responsive without exposing account context.
- Custom ingest handles the odd formats that banks do not document well.
PUBLIC VIEW
ledger documented private route hidden
importer healthy bank map hidden
store private schema hidden
review gated SSO required
The importer is the whole problem
The ledger itself is largely undramatic. The real engineering is upstream: every American bank exports data in a slightly different shape, occasionally changes that shape without warning, and rarely publishes a stable schema. The importer absorbs that chaos and presents clean, deduplicated transactions to the ledger.
The mental model that finally worked:
- Per-account adapter. One module per bank. The module knows that bank’s download format, its date format, its quirks (negative-as-debit vs. amount-with-sign), and produces a normalized row.
- Dedupe on (account, date, amount, memo-hash). Banks sometimes re-issue a transaction with a different ID when a payment clears. Those fields provide candidate matches, not proof of duplication. Ambiguous matches still need review; distinct transactions can share the same date and amount.
- Idempotent re-runs. Re-importing the same CSV must be a no-op. If it isn’t, you’ll spend a Saturday afternoon untangling double-counted rent.
Rules: classification at scale
Once transactions are in, classification is a problem you solve once and forget. The rules engine matches on description, amount, and source/destination account, then applies category, tag, and budget assignments.
IF description CONTAINS "wholefds"
AND amount > -800
THEN set category "Groceries"
add tag "WholeFoods"
assign budget "Food/Household"
Rules are useful for repeatable merchant descriptions. Ambiguous and unusual transactions still need review. Classification coverage should be measured against the actual records rather than advertised as a fixed percentage.
The queries that actually matter
Consistent transaction history lets you answer questions that no cloud finance app was ever going to surface, because surfacing them doesn’t sell more ads.
- What categories drift slowly upward year over year? (Subscriptions, every time.)
- What recurring charges did I sign up for and forget? (Group recurring descriptions across the last year; the small ones are the dangerous ones.)
- What’s my real monthly run-rate, smoothed across quarterly bills? (Median of trailing 90-day windows.)
- What does a tax return actually look like before TurboTax tries to upsell me? (Filter by tag, group by category, dump CSV, hand to accountant.)
A note on privacy and backups
This is the most sensitive data in the private runtime. Storage is encrypted at rest, backups land in more than one place, and recovery credentials live outside the runtime with a plan that does not depend on the site being up.
Backup verification runs monthly: a restore drill loads a fresh copy into an isolated environment, runs sanity checks, and reports the result. A backup you haven’t restored is a wish.
Takeaways
Self-hosting a finance ledger isn’t about being clever with money. It’s about being clear-eyed with it. The clarity arrives slowly — the first year is just setup, the second year is rules-tuning, by year three the system tells you things you wouldn’t have noticed without it. Five years of clean data lets you read your own life back in numbers, and the numbers are surprisingly honest.
Total ongoing cost: about $1.50 / month in storage and zero dollars in subscriptions. The replacement cost in cloud-finance products would be roughly $180 / year and the loss of any control over who sees what.