Building a Personal Finance Tracker Part 1. From Principles to Design
Building a Personal Finance Tracker App Part 1 — From Principles to Design
> I wanted to build an independent tracker, not just a household ledger. This is the journey of establishing financial principles and translating them into an app.
This series documents the process of creating Pomme, a personal finance tracking app. The name, inspired by Cézanne's apples, embodies the desire to see the essence. It's a single-user SPA built with React + Vite + Vercel, planned and deployed in just two days with Claude Code.
Part 1 covers the groundwork before building the app: establishing financial principles, defining the app concept, researching APIs, and designing the architecture.
Principles Come Before the App
When I decided to build a financial management app, the first thing I did wasn't open the code. I established financial principles first.
The reason is simple. If you don't know what to track, you can't design the app. Household ledger apps are everywhere; there's no reason to build another one. What I wanted was a tool that answered the question: "How close am I to financial independence right now?"
The framework of these principles is as follows.
Philosophy
> Money is the infrastructure of autonomy. > The goal is to create the freedom to choose.
Two Criteria for Judging Independence
- Track A — Investment assets exceed a certain threshold. The engine where money makes money is in place.
- Track B — A stable structure where I earn a certain monthly amount or more under my own name.
Meeting both tracks simultaneously signifies you can be independent. While income/expense tracking is necessary, the core was seeing how full both gauges are at a glance.
Not a Household Budget App, but an Independence Tracker
Once the principle was established, the app's identity became clear. Here's how it differs from existing household budget apps:
| General Household Budget App | Pomme |
|---|---|
| Focus on income/expense recording | Focus on progress toward independence |
| Goal is saving | Goal is asset growth + securing income structure |
| Detailed transaction history | Transaction history delegated to existing apps |
| Universal | Single-user, tailored to my principles |
The core screen is the dashboard. Opening the app reveals two gauges.
┌─────────────────────────────────┐
│ 🍎 Pomme │
│ │
│ Track A ━━━━━━━━━░░░░ 23% │
│ 투자자산 / 목표 │
│ │
│ Track B ━━░░░░░░░░░░░ 15% │
│ 월수입 / 목표 │
│ │
│ D-day ▸ 목표일까지 XXX일 │
└─────────────────────────────────┘
It's no exaggeration to say this is the app's entirety. The rest are screens for managing data to fill these gauges.
Feature Design — 5 Tabs
The dashboard alone is insufficient. Users must input and verify the data composing the gauges.
Dashboard — Track A/B gauges, D-day, monthly income/expense summary
Portfolio — Stock management (ticker, quantity, purchase price), real-time price checks, liquid assets
Cash Flow — Monthly income/expense trends, net worth change chart. Visualizes existing household ledger data
Roadmap — Roadmap to independence. Milestone timeline and action plan checklist
Settings — API connection settings, data export/import, theme
Where Does the Data Come From — API Research
I never intended to create a ledger where I manually input transaction details. I've been using Whooing, a double-entry ledger service, for years. I can pull income/expense data from Whooing and fetch stock prices via external APIs.
Whooing API
Whooing is a Korean double-entry household ledger service that provides a REST API. It uses OAuth 1.0-based authentication, and the main endpoints are as follows.
| Endpoint | Purpose |
|---|---|
accounts.json |
Item structure (income/expense/asset account tree) |
in_out.json |
Monthly income/expense changes |
mountain.json |
Monthly net asset trends |
bs.json |
Asset/liability balances |
sections.json |
Section (ledger unit) list |
Since it's double-entry bookkeeping, section_id is required for all requests, and dates use the YYYYMMDD integer format.
What if there's no Hooing API? Alternatives include manually importing CSV files or using bank Open Banking APIs. The key is to first decide "where to source the income/expense data."
Stock Quote API — Finnhub
I needed real-time US stock quotes. After comparing several APIs, I chose Finnhub for simple reasons:
- Free tier allows 60 requests per minute
- Supports real-time US stock prices (
/quote) + 7-day candle charts (/stock/candle) - Immediate access with a single API key
Yahoo Finance API is unofficial and unreliable, while Alpha Vantage's 5 requests per minute limit is too restrictive.
Exchange Rate API — ExchangeRate-API
To convert US stock prices to KRW, an exchange rate is needed. I chose ExchangeRate-API (open.er-api.com).
- Completely free, no API key required
- Just one line:
GET /v6/latest/USD - Extract
rates.KRWfrom the response
Architecture Design
With three APIs decided, the overall structure takes shape.
브라우저 (React SPA)
├── components/ ← 5페이지 UI
├── hooks/ ← 상태 관리 + 데이터 fetching
└── lib/ ← API 클라이언트 + 계산 로직
Vercel Functions (서버리스)
├── /api/whooing ← 후잉 프록시 (API 키 숨김)
├── /api/exchange-rate ← 환율 프록시 (6시간 캐시)
└── /api/data ← KV 읽기/쓰기 (디바이스 동기화)
외부 API
├── whooing.com ← 가계부 데이터
├── finnhub.io ← 미국 주식 시세
└── open.er-api.com ← USD/KRW 환율
Why a Server is Needed
A React SPA might seem sufficient, but there's a reason a server is required.
API key protection. Exposing Whooing OAuth tokens in the browser lets anyone view your household accounts. The server injects the key, and the client makes requests via a proxy like /api/whooing?endpoint=in_out.json.
Caching. Exchange rates only need updating every 6 hours. Caching them on the server reduces external API calls.
Device Synchronization. Using only localStorage means portfolios entered on a phone won't appear on a desktop. Storing data in Vercel KV (Redis) enables synchronization anywhere.
In contrast, Finnhub doesn't go through a server. Since users enter API keys directly in Settings, direct client calls are acceptable. Finnhub's free key is essentially at the level of public data anyway.
Server Proxy Pattern
[브라우저] [Vercel Function] [후잉 API]
│ │ │
├─ GET /api/whooing ──────────▶│ │
│ ?endpoint=in_out.json │── X-API-KEY 헤더 주입 ──────▶│
│ ¶ms=... │ │
│ │◀──────── 응답 ───────────────│
│◀──────── 응답 ───────────────│ │
The client only specifies which endpoint to call; the server handles all authentication. This pattern applies not just to Fwing but to any OAuth API.
Data Model Design
The data managed by the app falls into four main categories.
Portfolio — Stock holdings + Cash equivalents
Portfolio
├── holdings[] ← {ticker, shares, avgCost, currency}
├── cashAssets[] ← {name, amount, currency, excludeFromTrackA}
└── excludedAssets[] ← {name, amount, note} 트랙A 제외 자산
Goals — Goal setting
Goals
├── trackA: {target, label} 예) 투자자산 1.5억
└── trackB: {target, label} 예) 월수입 200만
Settings — API connections + App settings
Settings
├── whooingSectionId ← 후잉 섹션 선택
├── finnhubApiKey ← 주식 시세 API 키
├── cashAccountIds[] ← 현금성으로 볼 후잉 계좌
└── theme ← light / dark / system
SideIncome — Side income (automatically calculated by Hwing)
Track Calculation Logic
Once the data model is defined, the calculation formula for filling the gauge naturally follows.
Track A = Total investment assets
= SUM(종목별 shares × 현재가 × 환율)
+ SUM(현금성자산 중 excludeFromTrackA=false)
Current prices are fetched in real-time from Finnhub, and exchange rates from the ExchangeRate-API. Static data (holding quantity, purchase price) and dynamic data (market price, exchange rate) combine to form a single number.
Track B = This Month's Side Income
= 이번 달 후잉 총수입 - 본업 월급
Excluding the main job salary item from the household ledger leaves only "money earned under my name."
Data Storage Strategy
A database is excessive for a single-user app. Storage is divided into two layers.
Write-Through Cache Pattern
[쓰기]
유저 조작 → localStorage (즉시) → 1초 debounce → POST /api/data (KV)
[읽기]
앱 시작 → GET /api/data (KV) → localStorage 덮어쓰기
| Storage | Role |
|---|---|
| localStorage | Cache for immediate reads. Works offline |
| Vercel KV (Redis) | Device synchronization. Persistent storage |
If KV is unavailable, gracefully degrade to localStorage only. This enables the "enter on phone, view on desktop" experience without complex sync logic.
KV stores the entire {portfolio, goals, settings, sideIncome, exportedAt} as a single blob. Since it's a single-user app, one key is sufficient.
Technology Stack Selection
| Choice | Reason |
|---|---|
| React 19 + TypeScript | Component-based UI. Type safety |
| Vite | Fast HMR. Minimal setup |
| Vercel | Functions + KV + Deployment all in one place. Free tier sufficient |
| Recharts | React Native charts. Net worth trend, spending bar chart |
| Pretendard | Korean variable font. Clean number alignment |
The reason I didn't use Next.js is simple: I don't need SSR. What's the point of SEO for a one-person app? Static builds with Vite and using only Vercel Functions for serverless is much lighter.
Design Decisions
Just as important as features is whether it's an "app you want to open every day." Since it's a personal project, I went with my own taste.
- Atelier Tone — Background
#F8F7F4with paper texture (SVG noise). Workshop-like atmosphere instead of the cold feel typical of finance apps - Dark Mode —
[data-theme="dark"]CSS variable override. Warm gold accents - Gauge Animation — Counts up from 0 to current value. Gives a "filling up" sensation each time it opens
- CSS Variables — Manage all colors with variables. No hardcoding allowed
Part 1 Wrap-Up
I got this far without writing a single line of code. To summarize, this is the process I followed:
- Establish financial principles → Define what to track
- Finalize app concept → Not a household ledger, but an independence tracker
- Research external APIs → Hwing, Finnhub, ExchangeRate-API
- Design architecture → React SPA + Vercel Functions proxy
- Data model + storage strategy → Write-through cache
- Tech stack + design direction
If you want to build a personal finance app, I recommend starting by clarifying "What do I actually need?" before diving into code. This prevents losing direction mid-development.
Part 2 covers translating this design into actual code: dashboard UI, Finnhub real-time market integration, Hwing API proxy, and Vercel KV synchronization.