Quantum#
Self-hosted household finance for two. Bank data flows in read-only via SimpleFIN; identity is AT Protocol OAuth (sign in with your Bluesky handle); everything lives in one SQLite file on your own hardware.
Mint-shaped: categorize transactions (by rule or by hand, with full provenance of who or what categorized every transaction), then see monthly income vs. expense per category and net worth over time.
Requirements#
- Deno 2.9+
- A public HTTPS origin proxied to this app (existing Caddy works fine). The
ATProto authorization server must be able to fetch
https://your-domain/client-metadata.json, so LAN-only won't work. - A SimpleFIN Bridge account with your banks connected.
Setup#
-
Generate the OAuth signing key
deno task generate-key -
Configure the environment — copy
.env.exampleto.envand fill in:Variable Meaning APP_URLPublic HTTPS origin, e.g. https://quantum.example.com. Drives the OAuth client identity: changing it later forces both users to re-consent.ALLOWED_DIDSComma-separated DIDs allowed to log in (find yours at internect.info). Everyone else gets a 403. DB_PATHSQLite file path, e.g. ./data/quantum.db.OAUTH_PRIVATE_KEY_JWKOutput of deno task generate-key, single line. -
Caddy route — plain reverse proxy, no special paths:
quantum.example.com { reverse_proxy 127.0.0.1:3000 } -
Build and run
In a container (recommended for deployment; works with Podman or Docker):
podman build -t quantum . podman run -d --name quantum \ --env-file .env \ -p 3000:3000 \ -v quantum-data:/data \ quantumThe container reads
APP_URL,ALLOWED_DIDS, andOAUTH_PRIVATE_KEY_JWKfrom the environment and stores the database on thequantum-datanamed volume (DB_PATHdefaults to/data/quantum.dbinside). Back up that volume — it is the only state. To start on boot, generate a systemd unit (podman generate systemd --new quantum) or use a Quadlet.Or directly on the host:
deno install # once, after every dependency change deno task build PORT=3000 deno task startEither way, migrations apply automatically at startup. When running on the host, run from the repo root (the
migrations/directory and.envare resolved relative to it). -
First run — log in with an allowlisted handle, open Settings, paste a SimpleFIN setup token (single-use; generate it at the Bridge), and the first sync backfills what your banks provide (typically ~90 days). New accounts land in Accounts → Needs setup for one-time classification.
Day-to-day: a daily sync runs automatically (Deno.cron); Sync now lives in
Settings. Bank connections are managed at the Bridge, never in the app — Quantum
surfaces connection problems and links you there.
Development#
deno task dev # dev server on http://localhost:5173 (uses a loopback OAuth client)
deno task test # unit tests (sync, reconciliation, rules, reports)
deno task check # svelte-check
Troubleshooting: "unable to open database file"#
The container runs as the non-root deno user (uid 1000), so /data must be
writable by it:
- Named volume (
-v quantum-data:/data) — works out of the box; Podman and Docker copy the image's ownership into a fresh volume. - Bind mount (
-v /srv/quantum:/data) — the host directory keeps its host ownership. With rootless Podman, either add the:Uoption (-v /srv/quantum:/data:U, chowns to the container user) or runpodman unshare chown -R 1000:1000 /srv/quantumonce. On SELinux hosts (Fedora and friends) you also want:Z, i.e.:U,Z. - A volume first created by some other image/user keeps its old ownership;
podman volume inspectshows its path if you need to fix it up.
Operational notes#
- The database file is secret-grade. It contains the SimpleFIN Access URL (a permanent credential for read-only bank access) and your full financial history. Restrict file permissions and treat backups with the same care.
- Back up
DB_PATH(plus its-wal/-shmsiblings, or usesqlite3 .backup). Balance history powers the net-worth chart and cannot be re-fetched: losing the file loses it. Under Docker that means thequantum-datavolume. - Transaction backfill is limited to what banks return at first sync (~90 days). Balance snapshots start on day one and only grow.
- All money is stored as integer cents; 2-decimal currencies are assumed.
- Raw SimpleFIN responses are archived verbatim in
raw_syncs— normalization can be replayed against history if the schema evolves.