Tarek.
← All projects

2026

Ledgerly

A personal finance tracker that turns messy bank exports into clear, private insights — built local-first, sync-optional.

Next.jsTypeScriptPostgreSQLPrismaRecharts

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.

lib/import.ts
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

  1. Idempotency is a feature. Making destructive-feeling operations safely repeatable changed how users trusted the tool.
  2. Local-first is also a distribution strategy — privacy-conscious communities adopted it because of what it doesn't ask for.
  3. Server-rendered SVG goes a very long way before you need client-side charting.