Private, self-hosted household finance app. Canadian bank imports, no bank passwords.
See the codePrivate household finance, sorted on your own hardware.
FinVault is a self-hosted personal finance app for households that want clean budgets, statement imports, shared-cost tracking, and reports without handing bank credentials or transaction history to a cloud company. It is file-first for Canadian banks: download the QFX/OFX/QBO/QIF/CSV export or text-based PDF statement your bank already provides, preview it, import it, and sort the new lines.

Most finance apps ask for too much trust. FinVault is built around a quieter bargain:
Imported lines without a category land in an amber tray. Rules and remembered merchants suggest where they belong, and the monthly pigeonhole wall shows where spending is going.

FinVault handles OFX/QFX/QBO, QIF, CSV exports, and text-based PDF statements, including Canadian bank presets and a generic mapper for unusual files. Duplicates are skipped, overlapping date ranges are fine, and imports can be undone.

Search, filter, bulk edit, categorize, split, share, attach receipts, and export as CSV, OFX, or JSON.

Reports show income vs expenses, net worth, category breakdowns, budgets, goals, registered-plan reminders, tax summaries, and multi-currency warnings.

For Canadian accounts, FinVault is import-only by design. Canada does not yet have a live consumer open-banking system, and many aggregators fill that gap by asking for online-banking credentials. FinVault does not do that.
The flow is:
Canadian accounts are blocked from direct sync when the account country is CA, currency is CAD, or the provider institution looks Canadian.
git clone https://github.com/finvaultapp/finvault.git
cd finvault
cp .env.example .env
docker compose up -d --build
Open http://localhost:8000. The first account you create becomes the admin. Data is stored in the finvault-data Docker volume.
Optional services:
docker compose --profile postgres up -d
docker compose --profile ai up -d
docker compose exec ollama ollama pull llama3.1
If you expose FinVault beyond a trusted LAN, put it behind HTTPS and set:
COOKIE_SECURE=true
FORWARDED_ALLOW_IPS=<your reverse proxy IP or CIDR>
See .env.example for the full list.
| Setting | Default | Purpose |
|---|---|---|
REGISTRATION_MODE | invite | Who can sign up after the admin: open, invite, or closed. |
DEFAULT_CURRENCY | CAD | Suggested base currency for new members. |
COOKIE_SECURE | false | Set to true behind HTTPS. |
FORWARDED_ALLOW_IPS | Uvicorn default | Trust forwarded headers only from your reverse proxy. |
ALLOW_PRIVATE_OUTBOUND_URLS | false | Allow user-configured webhook/provider URLs to call LAN hosts. |
BANK_SYNC_ENABLED | false | Master switch for optional non-Canadian sync. |
SIMPLEFIN_ENABLED | false | Allows SimpleFIN setup tokens. |
AI_ENABLED | false | Enables the household AI endpoint. Members must still opt in. |
FX_FETCH_ENABLED | false | Enables fetching ECB exchange rates. |
FOLDER_IMPORT_ENABLED | false | Enables watched-folder imports. |
OCR_ENABLED | false | Enables local receipt OCR. |
BACKUP_DIR / BACKUP_KEEP | /data/backups / 14 | Where encrypted backups go and how many to keep (the passphrase is set in Admin). |
BACKUP_S3_* | empty | Optional S3-compatible copy of each backup. |
OIDC_ENABLED | false | Single sign-on; see below for the other OIDC_* settings. |
LOCAL_AUTH_ENABLED | true | Set to false to allow only single sign-on. |
PUBLIC_URL | empty | The address members open FinVault at. Needed (with SMTP) for "Forgot password?" emails. |
AUDIT_RETENTION_DAYS | 365 | How long audit events are kept. |
Everything is in the data volume. FinVault can also make encrypted backups for you (off by default):
.fvbackup file: a consistent database snapshot (SQLite's online backup API, or a JSON export of every table on Postgres), the receipts folder, secret.key and a manifest with a SHA-256 for every file, packed as tar.gz and encrypted with AES-256-GCM using a key derived from your passphrase with scrypt (random salt in the file header).BACKUP_DIR (default /data/backups; mount your NAS share there) and, if the BACKUP_S3_* settings are filled in, also to S3-compatible storage. The newest BACKUP_KEEP (default 14) are kept in each place.The passphrase is stored encrypted with the app's secret key and never sent back to the browser. If you set SECRET_KEY in .env instead of using the generated /data/secret.key, keep that value somewhere safe too: the backup only contains secret.key, and 2FA secrets and provider tokens are encrypted with the key.
The restore script checks everything before it overwrites anything, and refuses to run while FinVault has the database open.
docker compose stop finvault
# copy the backup into the data volume if it isn't there already
docker compose cp ./finvault-20260101-030000.fvbackup finvault:/data/backups/
# check the file and passphrase only (changes nothing)
docker compose run --rm finvault python -m scripts.restore_backup /data/backups/finvault-20260101-030000.fvbackup --verify-only
# restore (asks for the passphrase, then asks you to type "restore")
docker compose run --rm finvault python -m scripts.restore_backup /data/backups/finvault-20260101-030000.fvbackup
docker compose start finvault
Without Docker: cd backend && .venv/Scripts/python -m scripts.restore_backup FILE --data-dir ../data (use .venv/bin on macOS/Linux).
secret.key are kept next to them as *.before-restore-<time> (for Postgres, take a pg_dump first: rows are replaced in one transaction).finvault.db-wal behind, the script thinks the database is still open. Make sure the server is stopped, then add --force.FINVAULT_BACKUP_PASSPHRASE instead of typing it. A SQLite backup restores into SQLite; a Postgres (JSON) backup restores into Postgres or SQLite.FinVault can sign people in with Authentik, Pocket ID, Keycloak or any standard OpenID Connect provider. Create a confidential client at the provider with the redirect URI https://<your FinVault>/api/auth/oidc/callback, then set OIDC_ENABLED=true, OIDC_PROVIDER_NAME, OIDC_DISCOVERY_URL, OIDC_CLIENT_ID and OIDC_CLIENT_SECRET in .env. The sign-in page then shows "Sign in with ".
OIDC_ALLOW_SIGNUP=false (the default) only existing members can sign in, matched by the provider's verified email. The first sign-in links the provider account to the member, so later email changes at the provider can't take over another account. With true, new people get an account too, still following the registration mode (in invite mode, open the invite link first, then click the button).LOCAL_AUTH_ENABLED=false hides the password form and refuses password sign-in and registration. Keep at least one admin who can sign in through the provider before turning it off.Admin → Audit log lists security events: sign-ins and failed sign-ins (email and IP address, never passwords), 2FA on/off/reset, password changes, sign out everywhere, admin setting changes (which keys, never secret values), members created/deleted/changed, invites, backups, bank sync connect/disconnect and AI keys added/removed. Filter by event or member and export to CSV. Events older than AUDIT_RETENTION_DAYS (default 365) are deleted nightly. Behind a reverse proxy, the IP is taken from X-Forwarded-For (the Docker image runs uvicorn with --proxy-headers), so don't expose the container port directly if you rely on it.
X-FinVault header.Backend:
cd backend
python -m venv .venv
.venv/Scripts/pip install -r requirements-dev.txt
.venv/Scripts/python -m pytest
.venv/Scripts/python -m scripts.demo_seed
.venv/Scripts/python -m uvicorn app.main:app --reload
Frontend:
cd frontend
npm install
npm run dev
npm run build
npm audit --omit=dev
The Vite dev server proxies /api to localhost:8000. API docs are available at /api/docs while the backend is running.
The screenshots in docs/screenshots are generated from the synthetic demo household.
cd frontend
npm run build
# In another terminal, run the backend with demo data on http://127.0.0.1:8765
npm run screenshots
Demo credentials:
demo@finvault.local
demo-password-123
All demo transactions and balances are fake.
GitHub Actions (.github/workflows/ci.yml) runs on every push: backend tests, the frontend build and a production dependency audit, Playwright browser tests against the built app with demo data, and a Docker image build with a health-check smoke test.
cd backend && python -m pytest -q # API and importer tests
cd frontend
npm run build # the browser tests use the built app
npx playwright install chromium # once
npm run test:e2e # Playwright, Chromium only
npm run test:e2e seeds a demo household into a temporary data folder and starts backend/scripts/serve_local.py on port 8765 (E2E_PORT to change it, PYTHON to pick the interpreter with the backend requirements). To test a server that is already running, set E2E_BASE_URL instead. CI (.github/workflows/ci.yml) runs the backend tests, the frontend build, the browser tests and a Docker build with a health-check smoke test.
FinVault's interface takes inspiration from soft household sorting rooms and statement envelopes. Earlier visual exploration referenced Securo, but no Securo code is included.
Private, self-hosted household finance app. Canadian bank imports, no bank passwords.
See the codePrivate household finance, sorted on your own hardware.
FinVault is a self-hosted personal finance app for households that want clean budgets, statement imports, shared-cost tracking, and reports without handing bank credentials or transaction history to a cloud company. It is file-first for Canadian banks: download the QFX/OFX/QBO/QIF/CSV export or text-based PDF statement your bank already provides, preview it, import it, and sort the new lines.

Most finance apps ask for too much trust. FinVault is built around a quieter bargain:
Imported lines without a category land in an amber tray. Rules and remembered merchants suggest where they belong, and the monthly pigeonhole wall shows where spending is going.

FinVault handles OFX/QFX/QBO, QIF, CSV exports, and text-based PDF statements, including Canadian bank presets and a generic mapper for unusual files. Duplicates are skipped, overlapping date ranges are fine, and imports can be undone.

Search, filter, bulk edit, categorize, split, share, attach receipts, and export as CSV, OFX, or JSON.

Reports show income vs expenses, net worth, category breakdowns, budgets, goals, registered-plan reminders, tax summaries, and multi-currency warnings.

For Canadian accounts, FinVault is import-only by design. Canada does not yet have a live consumer open-banking system, and many aggregators fill that gap by asking for online-banking credentials. FinVault does not do that.
The flow is:
Canadian accounts are blocked from direct sync when the account country is CA, currency is CAD, or the provider institution looks Canadian.
git clone https://github.com/finvaultapp/finvault.git
cd finvault
cp .env.example .env
docker compose up -d --build
Open http://localhost:8000. The first account you create becomes the admin. Data is stored in the finvault-data Docker volume.
Optional services:
docker compose --profile postgres up -d
docker compose --profile ai up -d
docker compose exec ollama ollama pull llama3.1
If you expose FinVault beyond a trusted LAN, put it behind HTTPS and set:
COOKIE_SECURE=true
FORWARDED_ALLOW_IPS=<your reverse proxy IP or CIDR>
See .env.example for the full list.
| Setting | Default | Purpose |
|---|---|---|
REGISTRATION_MODE | invite | Who can sign up after the admin: open, invite, or closed. |
DEFAULT_CURRENCY | CAD | Suggested base currency for new members. |
COOKIE_SECURE | false | Set to true behind HTTPS. |
FORWARDED_ALLOW_IPS | Uvicorn default | Trust forwarded headers only from your reverse proxy. |
ALLOW_PRIVATE_OUTBOUND_URLS | false | Allow user-configured webhook/provider URLs to call LAN hosts. |
BANK_SYNC_ENABLED | false | Master switch for optional non-Canadian sync. |
SIMPLEFIN_ENABLED | false | Allows SimpleFIN setup tokens. |
AI_ENABLED | false | Enables the household AI endpoint. Members must still opt in. |
FX_FETCH_ENABLED | false | Enables fetching ECB exchange rates. |
FOLDER_IMPORT_ENABLED | false | Enables watched-folder imports. |
OCR_ENABLED | false | Enables local receipt OCR. |
BACKUP_DIR / BACKUP_KEEP | /data/backups / 14 | Where encrypted backups go and how many to keep (the passphrase is set in Admin). |
BACKUP_S3_* | empty | Optional S3-compatible copy of each backup. |
OIDC_ENABLED | false | Single sign-on; see below for the other OIDC_* settings. |
LOCAL_AUTH_ENABLED | true | Set to false to allow only single sign-on. |
PUBLIC_URL | empty | The address members open FinVault at. Needed (with SMTP) for "Forgot password?" emails. |
AUDIT_RETENTION_DAYS | 365 | How long audit events are kept. |
Everything is in the data volume. FinVault can also make encrypted backups for you (off by default):
.fvbackup file: a consistent database snapshot (SQLite's online backup API, or a JSON export of every table on Postgres), the receipts folder, secret.key and a manifest with a SHA-256 for every file, packed as tar.gz and encrypted with AES-256-GCM using a key derived from your passphrase with scrypt (random salt in the file header).BACKUP_DIR (default /data/backups; mount your NAS share there) and, if the BACKUP_S3_* settings are filled in, also to S3-compatible storage. The newest BACKUP_KEEP (default 14) are kept in each place.The passphrase is stored encrypted with the app's secret key and never sent back to the browser. If you set SECRET_KEY in .env instead of using the generated /data/secret.key, keep that value somewhere safe too: the backup only contains secret.key, and 2FA secrets and provider tokens are encrypted with the key.
The restore script checks everything before it overwrites anything, and refuses to run while FinVault has the database open.
docker compose stop finvault
# copy the backup into the data volume if it isn't there already
docker compose cp ./finvault-20260101-030000.fvbackup finvault:/data/backups/
# check the file and passphrase only (changes nothing)
docker compose run --rm finvault python -m scripts.restore_backup /data/backups/finvault-20260101-030000.fvbackup --verify-only
# restore (asks for the passphrase, then asks you to type "restore")
docker compose run --rm finvault python -m scripts.restore_backup /data/backups/finvault-20260101-030000.fvbackup
docker compose start finvault
Without Docker: cd backend && .venv/Scripts/python -m scripts.restore_backup FILE --data-dir ../data (use .venv/bin on macOS/Linux).
secret.key are kept next to them as *.before-restore-<time> (for Postgres, take a pg_dump first: rows are replaced in one transaction).finvault.db-wal behind, the script thinks the database is still open. Make sure the server is stopped, then add --force.FINVAULT_BACKUP_PASSPHRASE instead of typing it. A SQLite backup restores into SQLite; a Postgres (JSON) backup restores into Postgres or SQLite.FinVault can sign people in with Authentik, Pocket ID, Keycloak or any standard OpenID Connect provider. Create a confidential client at the provider with the redirect URI https://<your FinVault>/api/auth/oidc/callback, then set OIDC_ENABLED=true, OIDC_PROVIDER_NAME, OIDC_DISCOVERY_URL, OIDC_CLIENT_ID and OIDC_CLIENT_SECRET in .env. The sign-in page then shows "Sign in with ".
OIDC_ALLOW_SIGNUP=false (the default) only existing members can sign in, matched by the provider's verified email. The first sign-in links the provider account to the member, so later email changes at the provider can't take over another account. With true, new people get an account too, still following the registration mode (in invite mode, open the invite link first, then click the button).LOCAL_AUTH_ENABLED=false hides the password form and refuses password sign-in and registration. Keep at least one admin who can sign in through the provider before turning it off.Admin → Audit log lists security events: sign-ins and failed sign-ins (email and IP address, never passwords), 2FA on/off/reset, password changes, sign out everywhere, admin setting changes (which keys, never secret values), members created/deleted/changed, invites, backups, bank sync connect/disconnect and AI keys added/removed. Filter by event or member and export to CSV. Events older than AUDIT_RETENTION_DAYS (default 365) are deleted nightly. Behind a reverse proxy, the IP is taken from X-Forwarded-For (the Docker image runs uvicorn with --proxy-headers), so don't expose the container port directly if you rely on it.
X-FinVault header.Backend:
cd backend
python -m venv .venv
.venv/Scripts/pip install -r requirements-dev.txt
.venv/Scripts/python -m pytest
.venv/Scripts/python -m scripts.demo_seed
.venv/Scripts/python -m uvicorn app.main:app --reload
Frontend:
cd frontend
npm install
npm run dev
npm run build
npm audit --omit=dev
The Vite dev server proxies /api to localhost:8000. API docs are available at /api/docs while the backend is running.
The screenshots in docs/screenshots are generated from the synthetic demo household.
cd frontend
npm run build
# In another terminal, run the backend with demo data on http://127.0.0.1:8765
npm run screenshots
Demo credentials:
demo@finvault.local
demo-password-123
All demo transactions and balances are fake.
GitHub Actions (.github/workflows/ci.yml) runs on every push: backend tests, the frontend build and a production dependency audit, Playwright browser tests against the built app with demo data, and a Docker image build with a health-check smoke test.
cd backend && python -m pytest -q # API and importer tests
cd frontend
npm run build # the browser tests use the built app
npx playwright install chromium # once
npm run test:e2e # Playwright, Chromium only
npm run test:e2e seeds a demo household into a temporary data folder and starts backend/scripts/serve_local.py on port 8765 (E2E_PORT to change it, PYTHON to pick the interpreter with the backend requirements). To test a server that is already running, set E2E_BASE_URL instead. CI (.github/workflows/ci.yml) runs the backend tests, the frontend build, the browser tests and a Docker build with a health-check smoke test.
FinVault's interface takes inspiration from soft household sorting rooms and statement envelopes. Earlier visual exploration referenced Securo, but no Securo code is included.