← All projects

Expense Tracker

A multi-user personal-finance app built to practice production discipline, real auth, real data isolation, real migrations, real tests, with a few pragmatic ML features layered on top.

Last updated 2026-10-13 · 2 min read

Source on GitHub ↗

The problem

This is a personal expense tracker, and it's framed honestly: it's a product used to practice the discipline that separates a working demo from something that could hold real user data, proper multi-user authentication, per-user data isolation enforced at the query level, versioned schema migrations, and a real test suite, not a research artifact chasing a novel metric.

On top of that production base sit a few pragmatic ML features: natural-language expense entry, narrative spending insights, automatic categorization, anomaly detection, and short-term forecasting, each scoped to what a personal-finance tool actually needs rather than what's academically interesting.

How it works

The backend is FastAPI, deployed to Google Cloud Run, backed by Postgres in production and SQLite locally, with Alembic migrations versioning the schema (three migrations shipped so far, a baseline, one adding the user_id column and index for multi-user support, and one isolating the app onto a dedicated schema on a shared Supabase project after the original project went dark). Auth runs through Supabase Auth with dual-algorithm JWT verification: ES256 via a JWKS endpoint in production, HS256 for local and test tokens. Every one of the 15 non-health endpoints requires a valid token, and every database query filters on the current user's ID, cross-user access returns a 404, not a 403, so a stranger's data isn't even revealed to exist.

The Next.js 16 frontend deploys to Vercel and talks to the backend through a proxy that keeps Supabase's SSR cookie refresh working across redirects. On the ML side, a Groq-backed LLM parses free-text entries like "coffee 150" into structured expenses and writes short narrative insights, while three local models run without any external API call: a sentence-embedding categorizer, an IsolationForest anomaly detector, and a Prophet forecaster, each with documented low-confidence and fallback behavior, and each checked by a manual (non-CI) eval script rather than a CI-gated one.

Architecture

A standard multi-user web app shape, with ML features bolted on as optional endpoints rather than load-bearing infrastructure.

  1. Next.js 16 frontend

    Vercel, sign-in, expense CRUD, NL entry hero

  2. Supabase Auth

    JWT, ES256/JWKS in prod, HS256 locally

  3. FastAPI backend

    Cloud Run, 16 endpoints, all but /health require auth

  4. ML features

    Groq LLM

    NL parsing + narrative insights

    Sentence-embedding categorizer

    IsolationForest

    anomaly detection

    Prophet

    forecasting

  5. Postgres (prod) / SQLite (dev)

    per-user filtered queries, Alembic-migrated

Results, the honest numbers

Verified source

`expense-tracker/CURRENT_STATE.md:228-231`

commit 0908205 · measured 2026-06-01

Test suite · ruff clean, mypy clean (strict=false)

Cited in provenance.md

`expense-tracker/CURRENT_STATE.md:6-26,233-236`; outage: curl checks 2026-07-18; root cause: `gcloud logging read` + `gcloud run services/revisions describe`, `curl -I`, 2026-07-26; wave-19 fix verification: `git log` on `expense-tracker` + live `curl` checks, 2026-07-31

measured 2026-07-18

Deployment · backend found down 2026-07-18, root-caused and fixed 2026-07-26 (migrated off a dead Supabase project onto a dedicated schema on a shared one, /health returns 200 as of this writing); the frontend's Vercel deployment 404s (DEPLOYMENT_NOT_FOUND), a separate, still-open outage, said here rather than a dead link on the product card; 9/9 Playwright auth E2E scenarios pass locally

Cited in provenance.md

`expense-tracker/CURRENT_STATE.md:69-71,76-77`

verified against source 2026-10-10

Local ML features · evaluated with manual scripts (scripts/eval_parser.py, scripts/eval_ml.py), not gated in CI

What this means if you need something similar

If you need to evaluate whether someone can ship past a working demo to something that could hold real user data, this is the checklist worth applying: per-user isolation enforced at the query level (not just the UI), versioned migrations, and a real test suite, the boring production discipline a screenshot can't show you.