A full-stack app that categorizes personal spending from Alipay/WeChat exports: upload a CSV/Excel export → transactions are auto-categorized (trusted merchant rules first, ML suggestions for the rest) → review the leftovers → dashboards show where the money went.
User Browser (Next.js on Vercel)
↓ Supabase Auth (email/password) → JWT in Authorization header
FastAPI Backend (Railway)
↓ service-role key + explicit user_id scoping on every query
Supabase PostgreSQL + Auth + Storage
↓ RLS policies as defense-in-depth per-user isolation
- Frontend: Next.js 14 (TypeScript, App Router) + Tailwind CSS —
frontend/ - Backend: FastAPI + supabase-py —
backend/ - ML pipeline: scikit-learn (TF-IDF + Logistic Regression, optional
semantic-embedding second model with calibrated agreement) —
src/ - Database: PostgreSQL, 9 tables, RLS enabled on all —
supabase/migrations/
- Merchant rules (554 global seeds + per-user rules in
merchant_rules) are trusted: matching transactions get their category immediately (label_source='rule', no review needed). - The user's trained model suggests categories for everything else
(
label_source='model') — suggestions land in the Review Queue with a calibrated confidence. - Graduated trust: a model prediction auto-applies without review
(
label_source='model_agreed') only when the TF-IDF and semantic models agree, their calibrated confidence clears a data-derived threshold, and the prediction isn't the catch-all. Seedocs/FULL_AUDIT.mdfor why raw model confidence can't be trusted on unseen merchants.
Classification runs automatically after every upload (rules-only until a model
is trained) and re-runs after every training run (backend/ml.py).
Two very different questions:
- Stratified CV (known merchants): ~95% accuracy — but mostly memorization.
- GroupKFold by merchant (unseen merchants): far lower — this is the number
that matters for new data, and why model output defaults to review instead of
auto-applying. Details:
docs/FULL_AUDIT.md.
The ten original tabs are grouped into five compact sections with sub-tabs:
Overview (a monthly-spending line chart + category split), Transactions
(Upload / Label / Review queue), Model (Categories / Training), Planning
(Budget / Savings / Action plan), and Reports. A dismissible onboarding
checklist (Upload → Categories → Label → Train) guides new accounts. Plus a
separate Settings page (income, account deletion). The UI is a dark-first
design with a light theme toggle, skeleton loading states, and a marketing
landing page at / for signed-out visitors.
Correcting categories: the Reports → All transactions table is editable — click any category (including uncategorized rows) to reassign it; the change is saved immediately and flows through to the Overview and Budget views. Per-month budgets: the Budget tab has a month selector so you can see how each past month tracked against your budget (budgets are global, so past months compare against your current budget). Category colors: pick a color per category in Model → Categories (12 design-system choices, hex shown, no duplicates) — the same color follows that category everywhere: the Overview pie chart, badges in Budget/Review/Label, and Reports. Categories without a chosen color get a stable automatic color. The dashboard layout is fluid — it fills large desktop screens (capped for ultrawides) and adapts down to tablet and phone.
# 1. Backend
cd backend
pip install -r requirements.txt
cp .env.example .env # fill in your Supabase project's URL/keys/JWT secret
python -m uvicorn main:app --reload # http://localhost:8000
# 2. Frontend (new terminal)
cd frontend
npm install
cp .env.example .env.local # NEXT_PUBLIC_SUPABASE_URL/_ANON_KEY, NEXT_PUBLIC_API_URL
npm run dev # http://localhost:3000Guides: docs/guides/START_LOCAL.md (setup), docs/guides/TEST_LOCAL.md
(manual test flows), docs/guides/DEPLOYMENT.md (Railway + Vercel + Supabase).
pip install -r requirements-dev.txt
pytest tests/ # ML pipeline: parsing, routing, calibration, leakage guard
cd backend && pip install -r requirements-dev.txt
pytest tests/ # backend: JWT verification, cross-user isolationThere is no automated frontend test suite yet (open item); frontend is
verified with npx tsc --noEmit && npm run build.
financing/
├── frontend/ # Next.js app (see frontend/README.md)
├── backend/ # FastAPI app (see backend/README.md)
│ ├── routes/ # auth, categories, uploads, training, classify, dashboard, settings
│ ├── auth_utils.py # JWT verification against Supabase's JWKS
│ ├── tests/ # JWT verification + cross-user isolation tests
│ └── ml.py # per-user model loading + bulk classification
├── src/ # ML pipeline shared by backend + tests
│ ├── parse.py # Alipay/WeChat parsers → common schema
│ ├── segment.py # jieba tokenization + TF-IDF
│ ├── classify.py # rules-first + graduated-trust routing
│ ├── retrain.py # training entry point (both models + calibration)
│ ├── semantic.py # embedding classifier (Model2Vec / LSA fallback)
│ ├── calibration.py # top-label Platt scaling
│ ├── eval_grouped.py # GroupKFold evaluation + threshold derivation
│ └── merchant_categories.py # global rule patterns
├── supabase/ # migrations (schema, RLS, storage buckets, fixes)
├── tests/ # pytest suite for src/
├── scripts/test_local.sh
├── docs/ # context.md (project memory), audits, guides
└── data/ # gitignored user data + example templates
Full tree with explanations: REPO_STRUCTURE.md.
| Document | Purpose |
|---|---|
docs/context.md |
Project memory: every session, decision, and open item |
docs/PROJECT_SUMMARY.md |
Architecture overview and phase history |
docs/SECURITY_AUDIT.md |
Security checklist + test specifications |
docs/FULL_AUDIT.md |
ML integrity audit (merchant leakage, honest evaluation) |
docs/guides/ |
START_LOCAL, TEST_LOCAL, DEPLOYMENT, MIGRATION_GUIDE |
CLAUDE.md |
Collaboration guidelines for AI-assisted sessions |
- Old personal data is still recoverable from git history (pre-Session-19
commits); purging requires a
git filter-reporewrite + force-push. - Training runs in the backend threadpool — fine at small scale, should move to a real worker queue if user count grows.
- Real Postgres RLS policies are never exercised through the API (the backend
always uses the service-role key, which bypasses RLS) — only route-level
user_idscoping is tested today (backend/tests/). Testing RLS directly would need a local/CLI Supabase stack hit with an anon key + real user JWT.
MIT