Local-first, single-user personal budgeting app. Import CSVs from Star One Credit Union (checking + savings), categorize, and track envelope-style budgets — without handing your transactions to Plaid or a cloud service.
Status: Weekend 1 complete. Import pipeline works end-to-end (parse → dedup → snapshot → transactional insert → transfer-pair linking). See PLAN.md and CHANGELOG.md.
- Next.js 16 (App Router + Turbopack) · React 19 · TypeScript
- Tailwind v4 · shadcn/ui (base-nova style, Base UI primitives)
- better-sqlite3 + Drizzle ORM — local SQLite file at
./data/money.db - Vitest for parser/categorization unit tests
- pnpm · Node 24 (pinned via
.nvmrc)
No cloud. No auth. No Plaid. No deployment target — this runs on your machine.
nvm use # picks up Node 24 from .nvmrc
pnpm install
pnpm db:migrate # applies Drizzle migrations to ./data/money.db
pnpm dev # http://localhost:3000 → redirects to /importCreate an account (name, type, starting balance + date) from /import, then upload a Star One CSV export. The preview shows row counts, duplicates, pending rows, and errors; clicking Confirm import snapshots the DB, inserts the batch inside a transaction, and links transfer pairs across accounts.
| Command | What it does |
|---|---|
pnpm dev |
Start the dev server (this IS the app) |
pnpm test / test:watch / test:ui |
Vitest |
pnpm db:generate |
Generate a new Drizzle migration from src/db/schema.ts |
pnpm db:migrate |
Apply pending migrations |
pnpm db:studio |
Open Drizzle Studio |
pnpm lint |
ESLint |
src/
app/ Next.js 16 App Router pages (/import, /import/preview/[id], /import/success/[batchId])
components/ shadcn/ui + app components
db/ Drizzle schema + HMR-safe client singleton
lib/ parseCsv, normalize, hash, transferPair, snapshot, importBatch, pendingImport
drizzle/ Committed migration output
data/ money.db, pre-import snapshots, pending-import stash (gitignored)
.context/ Design artifacts, CSV samples, design deltas (gitignored)
These are load-bearing — the whole app is built around them:
- All money is stored as signed integer
amount_cents. Never floats. Withdrawals negative, deposits positive. - The CSV's signs are already correct.
Amount Debitis pre-negative,Amount Creditis positive. NoMath.abs, no sign flips by description. (This is the bug Plaid users keep hitting.) - Dedup is
(account_id, import_batch_id, import_row_hash), never Star One'sTransaction Number— they reuse6098for pending deposits across rows.import_row_hash = sha1(date | amount_cents | raw_description | raw_memo | row_index). - Transfer-pair detection is memo-independent. Two rows pair iff
|txn_a - txn_b| == 1AND same date AND|amount_a| == |amount_b|AND opposite signs AND different accounts. Star One labels the receiving-side memo correctly only ~20% of the time, so memo is confirmation-only. - Every batch import writes a DB snapshot first to
data/money.db.pre-import-{timestamp}. Last 10 are kept. Rollback = stop dev server, swap file.
Credit cards. Auth. Cloud sync. Multi-currency. Bill pay. Investment tracking. Tax features. Split transactions. YNAB-style overspend-shuffle. CI. Deployment.
- CLAUDE.md — guide for AI agents working in this repo (rules, conventions, Next.js 16 gotchas)
- PLAN.md — 5-weekend roadmap
- TODOS.md — short-term checklist
- CHANGELOG.md — release notes