Skip to content

AbstractNucleus/finance_tracker

Repository files navigation

finance

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.

What's inside

  • 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).

Stack

  • 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.yml for local dev
  • Anthropic SDK for the optional chat box and AI auto-categorize

No auth

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.

Quick start

Try it locally with seeded demo data

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:8888

Run against your own Postgres

cp .env.example .env
# edit DATABASE_URL to point at your Postgres
uvicorn app.main:app --host 0.0.0.0 --port 8888 --reload

The 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).

Docker, local dev

cp .env.example .env.dev     # or just leave it default
docker compose -f docker-compose.dev.yml up --build

Spins up a Postgres 16 alongside the app, both ephemeral.

Docker, production-style

cp .env.example .env
# set DATABASE_URL and optionally HOST_BIND_IP (e.g. a Tailscale IP)
docker compose up -d --build

Configuration

See .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

Tests and lint

pytest
ruff check app tests

CI runs the same on every push and PR. See .github/workflows/ci.yml.

Development

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.

License

MIT. See LICENSE.

About

Self-hosted expense and asset tracker for Nordea and Avanza CSVs. FastAPI, Postgres, Tailwind.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors