A small, self-hosted expense and asset tracker. Upload bank and broker CSVs, categorize transactions, watch budgets and trends.
I built this for myself against two Swedish providers: Nordea (bank) and Avanza (broker). The architecture is easy enough to adapt to other CSV formats.
- Overview: net worth line, monthly spending chart, balance, recent activity
- Nordea: transaction list with search, filter, bulk categorize, budgets
- Avanza: investment transactions and per-security performance
- Categories: manage categories, rules, and budgets
- Insights: coverage, spending clusters, subscriptions, transfers, cash-flow forecast, year-over-year
- AI chat: a built-in Claude chat box that can read and write app data via tools. Optional; needs an Anthropic API key or subscription token
- CSV export
Single dark theme. Server-rendered HTML. Vanilla JS. Numbers formatted in Swedish locale (kr).
- FastAPI (async) + Jinja2 templates
- PostgreSQL via SQLAlchemy 2.0 +
asyncpg. SQLite-in-memory for tests, SQLite file for the preview server - TailwindCSS v4 standalone CLI (no npm at runtime)
- Docker (multi-stage, non-root) for production,
docker-compose.dev.ymlfor local dev - Anthropic SDK for the optional chat box and AI auto-categorize
The app has no login. It's designed to sit behind a private network or VPN gate (Tailscale, WireGuard, an SSH tunnel, etc.) where network membership is the authentication boundary. Don't expose this app directly to the public internet without putting an auth layer in front of it.
make install # uv venv + pip install (Windows-flavored, see Makefile)
make seed # populate preview.db with realistic Swedish demo transactions
make dev # http://127.0.0.1:8888cp .env.example .env
# edit DATABASE_URL to point at your Postgres
uvicorn app.main:app --host 0.0.0.0 --port 8888 --reloadThe app does not create tables at runtime; it expects the schema (default namespace app_finance) to exist already. The SQLAlchemy models in app/models/ are the source of truth for what the schema looks like. Bring your own migration tool (Alembic, hand-written SQL, Supabase migrations, whatever).
cp .env.example .env.dev # or just leave it default
docker compose -f docker-compose.dev.yml up --buildSpins up a Postgres 16 alongside the app, both ephemeral.
cp .env.example .env
# set DATABASE_URL and optionally HOST_BIND_IP (e.g. a Tailscale IP)
docker compose up -d --buildSee .env.example. Vars:
| Var | Meaning |
|---|---|
DATABASE_URL |
asyncpg connection string |
DB_SCHEMA |
table namespace (default app_finance; "" for SQLite tests) |
HOST_BIND_IP |
host IP for the production docker port bind (default 0.0.0.0) |
ANTHROPIC_API_KEY |
optional, enables the chat box and AI auto-categorize |
pytest
ruff check app testsCI runs the same on every push and PR. See .github/workflows/ci.yml.
CLAUDE.md has the project conventions (async-by-default, schema-bound table args, etc). It's written for an AI pair programmer, but it works as a human orientation too.
MIT. See LICENSE.