mossfin
A calm, offline-first personal finance app — local by default, with optional self-hosted sync against your own Postgres and no user accounts.
mossfin tracks money the way you’d tend a garden — quiet, deliberate, nothing extra. It’s fully usable offline with no account, and syncs across devices only if you choose to self-host it.
Why I built this#
Most personal finance apps either want your bank credentials or want you to pay a monthly fee for a spreadsheet with graphs. I wanted something that works completely offline on one device, and only grows a network dependency (my own Postgres, my own token) if I actually want sync.
How it works#
Every write lands in IndexedDB first via Dexie, so the app is fully functional with no server and no account — that’s the default, not a degraded fallback mode. Sync is opt-in: point the app at a self-hosted server with a pre-shared SYNC_TOKEN, no user accounts involved, and press “Sync now” whenever you’re online to push local changes and pull anything new.
cp .env.example .env # set a real SYNC_TOKEN
docker compose up
The Postgres schema is applied automatically on first start via docker-compose init scripts, so self-hosting is a two-command setup rather than a manual migration exercise.
Technical challenges#
Offline-first as the default, not an afterthought#
Building sync as an optional layer on top of a fully-functional local app is a different design problem than building sync-first and degrading gracefully offline. Every mutation has to be safe to make with no network at all, then reconcilable later — not designed around a server round-trip that might fail.
Token auth without user accounts#
Self-hosting shouldn’t require running an auth system for a single-user app. A single pre-shared token per install keeps the server dumb and the setup minimal, at the cost of anything fancier than “one household, one token.”
Stack#
- TanStack Start + React for the app shell and routing
- Dexie (IndexedDB) as the local-first data layer
- shadcn/ui + Base UI for components
- Postgres, self-hosted via Docker, for optional sync
- Server functions for the sync push/pull surface
What this proves#
Mossfin’s bet is that “offline-first” should mean the app never needed a server, rather than that it just has a good offline cache. The finance domain makes that bet concrete: your money data stays on your device by default because that’s the only mode the app has, until you explicitly turn sync on.