Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Coffer

Self-hosted personal finance. Your accounts, cash flow, budget, net worth, and debts — in one SQLite file on your own machine, with no third party holding your data.

A coffer is the strongbox a household kept its money in.

Dashboard

Built for Canadian banking, where no single aggregator covers everything: Plaid handles the banks it can, a driven Chrome window exports CSVs from the banks it can't, and manual CSV import covers the rest. Everything lands in one deduplicated ledger.

Coffer is self-contained: a FastAPI backend, a static web bundle, and a SQLite file. No cloud services, no telemetry, no account signup — the only outbound calls are to Plaid (if you opt in) and to your banks (when you sync).

backend/   FastAPI + SQLModel + SQLite + Alembic           (Python 3.12, uv)
web/       SvelteKit (Svelte 5) + Tailwind v4 + LayerChart (Node 20+, pnpm)

Features

  • Dashboard — balances, recent activity, top spending at a glance.
  • Cash flow — income vs. spending by month; drill into any category or merchant. Reports by the date you actually spent, not the posted date.
  • Budget — month-to-date budget with targets auto-seeded from your own trailing spending, planned against projected income.
  • Net worth — assets and liabilities over time, per-account history.
  • Debts — every liability with its trajectory.
  • Rules — auto-categorize and rename merchants. Manual edits lock the transaction; no sync or rule ever clobbers them.
  • Transfers — internal moves are flagged and excluded from spending.
  • Local-first — one SQLite file under backend/data/. No cloud, no telemetry, no accounts. Delete the folder and it never existed.

How data gets in

Path Good for Effort per sync
Plaid Banks Plaid supports (e.g. TD, Amex) Zero — one click
Browser sync Banks Plaid can't hold (PC Financial, RBC) ~30 seconds — you approve the login, the driven Chrome window does the rest
CSV import Anything with an export (IBKR Flex Query, …) Download + upload, with column mapping and preview
Manual accounts Balance-only assets (pensions, property) Update the balance when you feel like it

All import paths feed the same deduplicating importer, so overlapping date ranges are free, and every CSV import can be undone as a unit.

Cash flow Budget
Cash flow Budget
Net worth Transactions
Net worth Transactions

Quick start

Prerequisites

  • uv — fetches a managed Python 3.12 on first sync if you don't have one
  • Node 20+ and pnpm (npm install -g pnpm)
  • Google Chrome — only if you enable browser sync (Playwright drives your installed Chrome; no separate browser download)

Windows PowerShell 5.1: run the snippets one line at a time — && doesn't chain there. If uv or pnpm say "not recognized" right after installing, reopen the terminal so PATH refreshes.

Windows — one command

.\coffer.ps1

First run does everything: installs backend deps, creates the database, copies .env.example to .env, installs web deps, builds the bundle, and serves the whole app on http://localhost:51823. Later runs just start it (rebuilding the bundle only when sources changed). .\coffer.ps1 -Dev gives hot-reload development instead.

macOS / Linux — manual

cd backend
cp .env.example .env
uv sync                          # creates .venv, installs deps
uv run alembic upgrade head      # creates data/finance.db

cd ../web
pnpm install
pnpm build

# serve everything from one port
cd ../backend
uv run uvicorn finance.main:app --host 0.0.0.0 --port 51823

For development, run the backend with --reload in one terminal and pnpm dev in another, then use http://localhost:5174 (proxies /api to the backend).

You can run without touching .env at all — Plaid and browser sync stay off until configured, and manual accounts + CSV import work out of the box.

Kick the tires with demo data

cd backend
uv run python scripts/seed_demo.py

Seeds seven months of realistic fake data — six accounts, ~300 transactions, balance history — so every screen has something to show (it's what the screenshots above are). It refuses to touch a database that already has real data; to clear it, stop the server and delete data/finance.db*.

First real run: add institutions and accounts under Accounts, then bring in transactions by any path below. Default categories are seeded automatically.

Connecting banks

Plaid

Plaid's Trial Plan is free with fast approval:

  1. Sign up at https://dashboard.plaid.com/signup and pick the Trial Plan.
  2. Team Settings → Keys — copy client_id and secret into backend/.env (PLAID_ENV=sandbox to test, production for real banks).
  3. Connecting an OAuth bank from a non-localhost address additionally needs an Allowed redirect URI in the Plaid dashboard and PLAID_REDIRECT_URI in .env, e.g. http://your-machine.your-tailnet.ts.net:51823/oauth-redirect.

Then Accounts → Connect a bank in the UI, and Sync now whenever you want fresh transactions.

Browser sync (PC Financial, RBC)

Some banks won't hold a Plaid connection — RBC's MFA method, for example, isn't supported at all. For these, Sync now opens a real, visible Chrome window on the bank's site: Chrome autofills the saved password, you approve the MFA challenge, and the app clicks through to the CSV export and imports it. The window runs from a dedicated Chrome profile in backend/data/ — never your daily browser profile — and that profile's saved password and "remember this device" cookie are what make later syncs quick.

One-time warm-up per bank:

cd backend
uv run python scripts/pc_walkthrough.py    # PC Financial
uv run python scripts/rbc_walkthrough.py   # RBC

Log in once, let Chrome save the password, tick "remember this device". Then set the matching *_SYNC_ENABLED and *_ACCOUNT_ID lines in .env (for RBC also RBC_ACCOUNT_LAST4 / RBC_ACCOUNT_KEY) — each is documented in .env.example. If the bank redesigns its site, the sync degrades gracefully: the window stays open and whatever CSV you download by hand gets imported.

Note: automated access to online banking may be against your bank's terms of service. Coffer drives your own browser, against your own accounts, on your own machine — but read your bank's terms and decide for yourself.

CSV import

Imports accepts any transactions CSV: the wizard sniffs the date, description, and amount columns (including debit/credit pairs), shows a preview, and lets you fix the mapping before committing. Re-importing an overlapping export is safe — duplicates are skipped — and every import can be undone from the past-imports list.

Scheduled syncs

Everything a Sync now click does is one API call, so any scheduler works:

curl -X POST http://localhost:51823/api/sync \
  -H "Content-Type: application/json" -d "{}"

Put that in Windows Task Scheduler or cron for a daily Plaid pull. One caveat: if browser sync is enabled, a global sync also opens the Chrome window, and a bank that challenges for MFA needs you at the machine — so either schedule it for a time you're around, or keep browser-synced banks on manual Sync now and let the schedule cover Plaid. Sync results land in /api/sync/logs and on the accounts screen either way.

Access from your phone — and your partner's

Coffer on a phone

Coffer has no authentication — it's a single-household app that trusts its network. Keep it on localhost or a private network like Tailscale; never port-forward it to the internet.

With Tailscale on both devices, open http://<your-machine>:51823 on your phone and tap Add to Home Screen — the PWA manifest is set up, so it installs as a standalone app with its own icon, indistinguishable from a native one. CORS allows http://localhost:* and the Tailscale CGNAT range out of the box.

Sharing with a partner is the same trick: invite them to your tailnet (Tailscale's free plan covers a household), install Tailscale on their phone, and they get the same URL and the same home-screen app. Everyone on the tailnet sees the same books — there are no per-person accounts, which for a shared household budget is the point.

To keep the backend running in the background on Windows: pm2 start ecosystem.config.cjs.


Data, privacy, and security

  • Everything lives in backend/data/: the SQLite database, backups, and the automation Chrome profile. The whole directory is gitignored.
  • The Chrome automation profile contains your saved bank passwords. Treat backend/data/ like a password vault: keep it out of cloud-synced folders and shared backups.
  • Plaid credentials stay in backend/.env (gitignored).
  • No telemetry, no analytics, no outbound calls except to Plaid (if configured) and your banks (when you sync).

Backup is one file:

sqlite3 backend/data/finance.db ".backup 'backups/finance-2026-07-09.db'"

Restore by copying the file back. There is no other state.

Project layout

backend/
  src/finance/
    routes/       dashboard, cash-flow, budget, net-worth, debts, transactions,
                  accounts, categories, rules, imports, sync, plaid
    services/     plaid_client, sync, csv_import (dedup core), categorize,
                  rules, pc_financial_sync, rbc_sync, browser_profile
    models.py     SQLModel tables — money as signed integer cents, dates UTC
    seed.py       default categories on first boot
    config.py     pydantic-settings; everything overridable via .env
  migrations/     Alembic
  scripts/        bank walkthroughs + one-off utilities
web/
  src/routes/     one folder per screen
  src/lib/        typed API client, formatting, components, charts

Design invariants worth knowing before you patch anything:

  • Money is signed integer cents, positive = inflow. Plaid's opposite sign convention is inverted at the sync boundary.
  • Datetimes are stored UTC; day boundaries ("today", budget month-to-date) use the TIMEZONE setting.
  • User edits are never overwritten by syncs or rules. Soft delete only.

Dev commands

cd backend
uv run ruff check .            # lint
uv run ruff format .           # format
uv run alembic revision --autogenerate -m "..."   # new migration

cd web
pnpm check                     # svelte-check
pnpm lint                      # prettier + eslint

Roadmap

  • Generic assisted browser sync: point it at any bank's login URL, download the CSV yourself, let the importer catch the file — no per-bank code.
  • Recordable per-bank export recipes (JSON steps) instead of hardcoded selectors, shareable as community files.
  • In-app settings and first-run onboarding instead of .env editing.
  • One-command setup for macOS/Linux, and a Docker image covering the Plaid + CSV paths (browser sync needs a visible Chrome, so it stays native).
  • Built-in sync scheduler with per-institution cadence.
  • Configurable currency/locale (currently formats as CAD).

License

MIT

About

Local-first personal finance: cash flow, budget, net worth, and debts in one SQLite file. Plaid where it works, browser-automated CSV export where it doesn't.

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages