Two weeks ago, I decided to build a personal finance app. Not because the world needs another budget tracker — there are hundreds. I built it because every single one of them wanted my bank credentials, my email, or $99/year.
I'm a developer. I know what happens to data on servers. I've seen breaches. I've read the incident reports. And the idea of sending my salary information to someone's PostgreSQL instance — even encrypted — made me uncomfortable.
So I made a decision that shaped everything else: zero backend. Zero server. Zero network requests for financial data. Everything stays in the browser.
This is the story of what that decision cost me, what it gave me, and what I'd do differently.
The Idea: Plan vs. Reality
Here's what annoyed me about budgeting apps: they all track what you spent. Past tense. You open the app, see a number, feel bad, close the app.
I wanted something different. I wanted to see my plan next to my reality — in real-time. Not "you spent $400 on food this month" but "you planned $350 for food, you've spent $280 with 10 days left, you're on track."
That's DonFlow. A plan-vs-reality budget tracker that runs entirely in your browser.
The tech stack is straightforward: React 19, TypeScript, Vite, Tailwind CSS v4, and Dexie (a wrapper around IndexedDB). No Express. No Firebase. No Supabase. No API routes. Nothing.
React 19 + TypeScript
Tailwind CSS v4
Dexie (IndexedDB wrapper)
Vite 7
GitHub Pages (hosting)
---
Monthly hosting cost: $0
Day 1-3: The Database Decision That Changed Everything
The first real decision was storage. I considered:
- localStorage — Simple, but 5-10MB limit and no indexing. For a finance app that could accumulate thousands of transactions? No.
- SQLite via WASM — Powerful, but the bundle size (400KB+) bothered me for what's supposed to be a lightweight app.
- IndexedDB via Dexie — Async, indexed, virtually unlimited storage, and Dexie gives you a clean API.
I went with Dexie and haven't looked back.
Here's what the schema looks like:
class DonFlowDB extends Dexie {
accounts!: EntityTable<Account, 'id'>
transactions!: EntityTable<Transaction, 'id'>
categories!: EntityTable<Category, 'id'>
budgets!: EntityTable<Budget, 'id'>
merchantRules!: EntityTable<MerchantRule, 'id'>
recurringItems!: EntityTable<RecurringItem, 'id'>
insights!: EntityTable<Insight, 'id'>
constructor() {
super('DonFlowDB')
this.version(4).stores({
accounts: '++id, name, type, isActive',
transactions: '++id, accountId, categoryId, date, type, csvHash',
budgets: '++id, categoryId, month, [categoryId+month]',
merchantRules: '++id, merchantPattern',
recurringItems: '++id, name, type, isActive',
insights: '++id, type, month, isRead',
})
}
}
The compound index [categoryId+month] on budgets was a lesson learned the hard way. My first version queried all budgets and filtered in JavaScript. With 12 months × 15 categories, that's 180 records to scan every time the dashboard loads. The compound index made budget lookups instant.
What I didn't expect: Dexie's liveQuery with dexie-react-hooks basically gives you real-time reactivity for free. Change a transaction, and every component watching that table re-renders. It feels like having a real-time database — except it's all local.
// This re-renders automatically when transactions change
const transactions = useLiveQuery(
() => db.transactions
.where('date')
.between(startOfMonth, endOfMonth)
.toArray()
)
Day 4-7: The CSV Parsing Nightmare
This is where I spent the most time and had the most failures.
In South Korea, every bank and credit card company exports CSVs differently. I'm not talking about minor differences. I mean fundamentally different structures:
- Some put the date in column A. Others in column C.
- Some use "출금" (withdrawal) and "입금" (deposit). Others use positive/negative numbers.
- Some include the merchant name. Others include a transaction code you need to look up.
- Some export as UTF-8. Others as EUC-KR (because it's 2026 and apparently encoding wars never ended).
I tried to build a universal CSV parser. I failed. Twice.
First attempt: Column name matching. Map known header names to fields. Broke immediately because Samsung Card calls the amount column "이용금액" while Shinhan Card calls it "이용금액(원)". One extra suffix and the matcher fails.
Second attempt: Position-based mapping with bank presets. Created a config for each bank format. This worked... until I realized banks silently change their CSV export format. A user exports from Hana Card, and suddenly there's an extra column they didn't have last month.
Final approach: Hybrid auto-detection. The app tries header matching first, falls back to pattern recognition (looking for date-like columns, number-like columns), and lets the user manually map columns if both fail. It's not elegant, but it's honest.
// Auto-detect which columns contain what
function detectColumns(headers: string[], rows: string[][]) {
const dateCol = headers.findIndex(h =>
/날짜|일자|date|거래일/i.test(h)
)
const amountCol = headers.findIndex(h =>
/금액|amount|이용금액|출금/i.test(h)
)
const merchantCol = headers.findIndex(h =>
/가맹점|merchant|이용처|적요/i.test(h)
)
// Fallback: scan actual data for patterns
if (dateCol === -1) {
// Find column where most values match date patterns
}
return { dateCol, amountCol, merchantCol }
}
The XLSX support (via SheetJS) was easier than CSV, ironically. At least Excel files have consistent structure.
Day 8-10: Merchant Classification — The Fun Part
This was actually enjoyable. When you import transactions, the merchant names are usually cryptic. "카카오페이*배달의민족" doesn't scream "food delivery" to a budgeting algorithm.
I built a keyword-based classifier with 100+ Korean merchant rules:
const MERCHANT_RULES: Record<string, string> = {
'배달의민족': '식비', '요기요': '식비', '쿠팡이츠': '식비',
'스타벅스': '카페', '투썸플레이스': '카페', '메가커피': '카페',
'카카오T': '교통', '티머니': '교통',
'넷플릭스': '구독', 'Spotify': '구독', 'ChatGPT': '구독',
'쿠팡': '쇼핑', '네이버쇼핑': '쇼핑',
// ... 100+ more rules
}
Then I added a learning layer. When a user manually categorizes a transaction, DonFlow remembers the merchant-to-category mapping in a merchantRules table. Next time the same merchant appears, it auto-categorizes. The more you use it, the smarter it gets — without any ML, without any server, without sending your purchase history anywhere.
The part I'm proud of: The classifier checks user rules first (personalized), then falls back to built-in rules. Your corrections always take priority.
Day 11-13: The Budget Drift System
This is DonFlow's core feature, and the part that actually makes it useful for me personally.
Every month, you set budget targets per category. As you log transactions (manually or via import), the dashboard shows progress bars — planned vs. actual. When you're approaching the limit, the bar turns yellow. When you're over, it turns red with a drift warning.
But here's the subtlety: it's not just about whether you went over budget. It's about the rate of spending. If you've spent 80% of your food budget by day 10, that's a problem even if you're technically "under budget." The pace is wrong.
The what-if simulator was a late addition but turned out to be valuable. You can ask "what if I reduce my café budget by $50 and add it to groceries?" and see the projected impact based on your historical spending patterns.
What failed here: I originally tried to calculate trends (month-over-month spending patterns, projected end-of-month totals). The math wasn't hard, but the UX was. Showing someone "you're projected to overspend by $127.50" when it's February 5th felt alarmist and inaccurate. I ripped it out and replaced it with simpler, more honest indicators.
Day 14: The "Is This Actually Useful?" Moment
Two weeks in, I used DonFlow to review my January spending. I imported three CSV files (two credit cards, one debit card), manually categorized maybe 20% of transactions (the classifier handled the rest), and looked at the dashboard.
I was spending 40% more on "구독" (subscriptions) than I'd budgeted. Not because any single subscription was expensive, but because they'd accumulated. ChatGPT Plus here, YouTube Premium there, iCloud storage, Spotify, two streaming services I forgot I had.
That's the moment the app justified itself. Not the tech. The insight.
What I'd Do Differently
1. Start with import, not manual entry. I built the manual transaction entry form first. Nobody wants to type in every coffee purchase. CSV/XLSX import should've been day one.
2. Don't over-engineer the onboarding. My first version had a 5-step setup wizard (add accounts → set categories → define budgets → import data → review). Now it's: open the app, import a file, done. Categories and budgets can come later.
3. Test with real data earlier. I spent days building features with fake test data. The moment I loaded real bank exports, half my assumptions broke. Date formats, encoding, column layouts — all different from what I expected.
The Privacy Argument
People ask: "Why not just use a server? It would make sync so much easier."
They're right. Multi-device sync is the biggest feature I'm missing. But here's my counterargument:
Open DonFlow. Open DevTools. Go to the Network tab. Reload the page. Use the app for 5 minutes.
Zero network requests. Not one. No analytics. No error tracking. No telemetry. Your salary, your spending habits, your subscriptions — none of it leaves your machine.
In a world where finance apps get breached, where budget apps sell anonymized spending data to advertisers, and where "encrypted" just means "encrypted until we get subpoenaed" — running entirely client-side is a feature, not a limitation.
The hosting cost is $0 (GitHub Pages). There's no server to breach because there's no server. There's no database to leak because the database is on your machine.
What's Next
I'm preparing to share this on Show HN soon. Before that, I'm working on:
- Multi-currency support — I live in Korea but have USD expenses
- Better recurring transaction detection — Auto-detecting subscriptions from patterns
- PWA offline mode — The app already works offline (it's all client-side), but proper service worker caching would make it feel native
If you're curious, you can try it right now:
🔗 DonFlow Live Demo — loads instantly, no signup needed
📖 Source Code on GitHub — MIT licensed, every line auditable
If You're Building Too
I build tools, games, and resources for developers — all free or pay-what-you-want:
🛠️ 27+ Free Developer Tools — JSON formatter, UUID generator, password analyzer, and more. All browser-based, no signup.
🎮 27 Browser Games — Built with vanilla JS. Play instantly, no install.
📚 Developer Resources on Gumroad — AI prompt packs, automation playbooks, and productivity guides.
Building in public is uncomfortable. You share the ugly parts. But two weeks in, DonFlow already caught a subscription leak I'd missed for months. Sometimes the tool you build for yourself turns out to be the one other people needed too.
What's the last thing you built for yourself that surprised you?
📖 Series: Building a Finance App With No Server
- I Built a Finance App With Zero Backend — Browser-Only Architecture
- Why I Chose IndexedDB Over a Backend
- Storing Financial Data in the Browser: IndexedDB + Dexie.js Guide
- Share Your Web App State via URL — No Backend Required
- → You are here — What I Learned After 2 Weeks of Building
📘 Free Resource
If you are building with a $0 budget, I wrote a playbook about what works, what doesn't, and how to think about the $0 phase.
📥 The $0 Developer Playbook — Free (PWYW)
Want the deep dive? The Extended Edition ($7) includes a 30-day launch calendar, 5 copy templates, platform comparison matrix, and revenue math calculator.
Top comments (0)