The problem
Every finance tool I tried had the same two flaws: it demanded my banking credentials, and it buried simple answers under dashboards I never asked for. All I wanted was to answer "where did my money actually go this quarter?" without handing over my login.
Ledgerly is the answer I built for myself: import a CSV export from any bank, and get clean categorization, monthly trends and budget tracking. No account linking, no cloud requirement.
Approach
Local-first data
The core decision shaped everything else: your ledger lives in SQLite locally by default, with an opt-in Postgres sync for people who want multi-device access.
export async function importTransactions(raw: string) {
const rows = parseCsv(raw);
return db.transaction(async (tx) => {
for (const row of rows) {
await tx.transaction.upsert({
where: { fingerprint: row.fingerprint },
create: row,
update: {}, // re-imports are idempotent
});
}
});
}Re-importing the same file twice creates no duplicates — each row gets a content fingerprint used as an upsert key. Small detail, but it's what makes imports safe to repeat.
Categorization you can correct
Auto-categorization uses a small rules engine (merchant regexes learned from your own corrections) rather than ML. It's transparent and debuggable:
{
"match": { "merchant": "AH|Albert Heijn" },
"category": "groceries",
"confidence": 0.98
}When a rule is wrong, you fix the category once and Ledgerly offers to save the correction as a new rule.
Rendering performance
The trends view renders thousands of points. Rather than pulling in a heavy chart library wholesale, charts are server-rendered to SVG paths and hydrated with interaction only on hover targets.
Outcome
- ~2k monthly active users (mostly friends-of-friends and r/selfhosted)
- Median import-to-insight time: under 30 seconds
- Zero support tickets about duplicate transactions since the fingerprint system shipped
What I learned
- Idempotency is a feature. Making destructive-feeling operations safely repeatable changed how users trusted the tool.
- Local-first is also a distribution strategy — privacy-conscious communities adopted it because of what it doesn't ask for.
- Server-rendered SVG goes a very long way before you need client-side charting.