Understand your personal finances. Forget Excels, try Whisper Money.
Go to file
Víctor Falcón eb60f2eb30
fix(open-banking): unblock account mapping when the bank reports accounts without a uid (#746)
## Problem

A paying user reported that "Save & Sync" on the account mapping screen
did nothing — no error, no navigation, nothing.

EnableBanking returned their Société Générale card (`CB Visa`) with
`uid: null`:

```json
{"uid": null, "name": "CB Visa", "cash_account_type": "CARD", "account_id": {"iban": null, "other": {"scheme_name": "CPAN", "identification": "************8210"}}}
```

The page renders one mapping row per pending account, so the form posted
`bank_account_uid: null`. `MapAccountsRequest` requires it, the request
422'd, and since the page never rendered validation errors the button
looked dead. The user retried the connection six times.

A second problem kept them from doing what they actually wanted: both
accounts reported `currency: "XXX"` — ISO 4217 for "no currency".
`getCompatibleAccounts` filtered their existing EUR accounts against
`"XXX"`, matched nothing, and the "Link to existing account" option was
never rendered. The backend already knew `XXX` isn't a currency
(`AccountUserCurrencyService::resolveImportedCurrency`); the frontend
didn't.

## Fix

- Accounts without a uid are filtered out of the mapping screen. They
can never be synced anyway — every sync service early-returns on an
empty `external_account_id` — and `CreatesAccountsFromPending` already
skipped them during onboarding. That rule now lives in one place,
`BankingConnection::mappablePendingAccounts()`.
- The skipped accounts are named on the page, so users don't go hunting
for an account they can see in their bank app.
- A connection where *no* account has a uid is closed out instead of
parking on a mapping page that could only ever 422.
- Validation errors surface as a toast, with app-authored messages in
`MapAccountsRequest::messages()` instead of raw field paths.
- `XXX` (and a blank/lowercase variant) is treated as "unknown
currency": the code isn't displayed, and it no longer filters out every
linkable account.

## Scope

One user affected in production (the reporter). Their connection has one
valid account alongside the card, so this fix unblocks them on deploy —
no data change needed.

## Testing

- `tests/Feature/OpenBanking` — 315 passed, including two new cases:
uid-less accounts are hidden and named, and an all-uid-less connection
is closed rather than left awaiting mapping.
- Browser QA against a local reproduction of the exact production
payload: only `Compte Bancaire` is offered, `CB Visa` is named as
skipped, "Link to existing account" appears despite the `XXX` currency,
submitting without picking an account toasts the error, and picking
`Dany SG` links it (`external_account_id` set, connection `active`).

## Demo


https://github.com/user-attachments/assets/6275834a-a518-47b9-9015-a698e5926d71
<!-- PLACEHOLDER: drag account-mapping-fix-qa.mp4 here -->

## Not in this PR

`AuthorizationController::refreshAccountIds()` consumes the raw pending
list on reconnect; its positional fallback can pair a legacy account
with a uid-less entry. Not reachable for any account in production
today, it belongs to a different flow, and it deserves its own test —
filed separately rather than smuggled in here.
2026-08-09 18:41:35 +02:00
.agents/skills feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
.claude chore: add /release slash command (#738) 2026-07-26 16:00:45 +00:00
.cursor chore: add sentry mcp (#300) 2026-04-17 10:42:34 +02:00
.github fix(ci): publish the production image as :latest so Docker/Coolify deploys work (#706) 2026-07-21 08:28:42 +00:00
.opencode/skills chore: Update larevel boot package 2026-01-27 10:55:46 +01:00
.pi fix(automation): avoid rule preview n+1 (#431) 2026-05-26 08:02:46 +02:00
app fix(open-banking): unblock account mapping when the bank reports accounts without a uid (#746) 2026-08-09 18:41:35 +02:00
bootstrap fix(oauth): stop reporting client-facing OAuthServerException to Sentry (#723) 2026-07-22 12:40:24 +00:00
config feat(currency): add THB (Thai Baht) (#740) 2026-08-09 14:24:58 +00:00
database fix(onboarding): don't trap users on the syncing step when a bank sync fails (#745) 2026-08-09 18:40:49 +02:00
docker fix(docker): keep the client port in the URLs the app generates (#739) 2026-08-09 14:25:19 +00:00
docs docs: document adding a new currency (#681) 2026-07-15 12:32:18 +00:00
experiments feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
lang fix(open-banking): unblock account mapping when the bank reports accounts without a uid (#746) 2026-08-09 18:41:35 +02:00
public revert(pwa): drop handle_links to keep app deep-linking (#708) 2026-07-21 13:40:13 +02:00
resources fix(open-banking): unblock account mapping when the bank reports accounts without a uid (#746) 2026-08-09 18:41:35 +02:00
routes feat(budgets): add per-budget email notifications (#731) 2026-07-24 12:52:03 +02:00
screenshots fix: Add gap between filter/create button on mobile settings pages (#115) 2026-02-12 20:50:05 +01:00
scripts chore: release v0.2.5 (#539) 2026-06-15 16:48:25 +00:00
src/lib/crypto E2E Encryption 2025-11-07 14:21:25 +00:00
storage feat: Enable email verification on sign up (#97) 2026-02-03 10:15:07 +01:00
templates/coolify fix: split drip and default email senders (#263) 2026-04-06 12:16:47 +02:00
tests fix(open-banking): unblock account mapping when the bank reports accounts without a uid (#746) 2026-08-09 18:41:35 +02:00
.dockerignore fix: publish and use production Docker image (#393) 2026-05-20 07:19:31 +00:00
.editorconfig Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
.env.example fix(auth): make build deterministic when REGISTRATION_ENABLED=false (#720) 2026-07-22 07:04:20 +00:00
.env.production.example refactor: remove HIDE_AUTH_BUTTONS launch gate and waitlist apparatus (#717) 2026-07-22 08:51:48 +02:00
.gitattributes Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
.gitignore chore: ignore .playwright-mcp directory (#511) 2026-06-09 12:05:21 +02:00
.mcp.json chore: add sentry mcp (#300) 2026-04-17 10:42:34 +02:00
.php-cs-fixer.dist.php Execute browser tests on CI (#10) 2025-12-03 16:26:30 +01:00
.php-version chore: ignore local .php-version 2026-02-07 18:48:04 +01:00
.prettierignore Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
.prettierrc Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
.release-it.json chore: release v0.2.5 (#539) 2026-06-15 16:48:25 +00:00
AGENTS.md chore: update Laravel Boost skills and guidelines (#521) 2026-06-12 18:20:30 +02:00
CHANGELOG.md chore: release v0.2.7 (#737) 2026-07-26 17:14:23 +02:00
CLAUDE.md docs: document running the dev server for QA (#677) 2026-07-14 21:38:11 +00:00
Dockerfile chore: Simplify IndexedDB sync by moving to Inertia shared props (#63) 2026-01-19 19:15:26 +01:00
Dockerfile.production fix(docker): keep the client port in the URLs the app generates (#739) 2026-08-09 14:25:19 +00:00
LICENSE.md Add Creative Commons license 2025-11-26 17:07:20 +01:00
LOCALIZATION.md feat: Spanish localization (#74) 2026-02-08 11:58:08 +01:00
ONBOARDING.md fix(banking): handle balance-fetch timeouts and silence handled retries (#450) 2026-05-29 14:58:38 +02:00
README.md fix(auth): make build deterministic when REGISTRATION_ENABLED=false (#720) 2026-07-22 07:04:20 +00:00
artisan Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
autoresearch-dashboard.md feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
autoresearch.jsonl feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
autoresearch.md feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
autoresearch.sh feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
boost.json feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
bun.lock fix(chart): upgrade recharts to 3.9.2 to stop the mobile dashboard render loop (PHP-LARAVEL-47) (#659) 2026-07-08 12:12:20 +00:00
components.json Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
compose.yaml chore: replace Caddy with Portless for local HTTPS proxy (#258) 2026-04-02 16:39:44 +01:00
composer.json feat(mcp): add OAuth 2.1 for Claude Desktop & ChatGPT connectors (Phase 3) (#691) 2026-07-17 19:10:48 +02:00
composer.lock feat(mcp): add OAuth 2.1 for Claude Desktop & ChatGPT connectors (Phase 3) (#691) 2026-07-17 19:10:48 +02:00
docker-compose.production.yml Add production Docker setup for easy self-hosting with the CI-built image (#42) 2025-12-30 07:22:19 +01:00
eslint.config.js Y3:0 2025-11-26 12:01:49 +01:00
falcode.json Add falcode config file 2026-03-11 15:28:52 +01:00
opencode.json chore: add sentry mcp (#300) 2026-04-17 10:42:34 +02:00
package-lock.json chore: release v0.2.7 (#737) 2026-07-26 17:14:23 +02:00
package.json chore: release v0.2.7 (#737) 2026-07-26 17:14:23 +02:00
phpstan-baseline.neon fix(static-analysis): clear phpstan-baseline by fixing all suppressed errors (#183) 2026-03-02 12:22:30 +00:00
phpstan.neon feat(mcp): read-only MCP server for Pro accounts (#689) 2026-07-17 16:54:15 +02:00
phpunit.xml feat: use testcontainers for isolated MySQL in test runs (#153) 2026-02-25 10:14:20 +01:00
tsconfig.json Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
vite.config.ts chore(sentry): migrate Vite source map upload from Bugsink to Sentry (#630) 2026-07-03 13:36:13 +00:00
vitest.config.ts Fix cashflow null category rows (#382) 2026-05-11 18:54:26 +02:00
vitest.setup.ts feat: add multiple chart view modes for net worth evolution (#37) 2025-12-30 07:22:19 +01:00
whispermoney fix: Wrong whispermoney script path 2026-01-16 18:48:44 +01:00
worktree.sh feat(mcp): add OAuth 2.1 for Claude Desktop & ChatGPT connectors (Phase 3) (#691) 2026-07-17 19:10:48 +02:00

README.md

Whisper Money

Deutsch | Español | français | 日本語 | 한국어 | Português | Русский | 中文

Whisper Money

CC BY-NC 4.0

The most secure way to understand your finances.

Whisper Money is a privacy-first personal finance application that helps you track, categorize, and understand your spending—all while keeping your financial data encrypted and secure.

🎮 Try the Demo: Experience Whisper Money with our demo account - no registration required!

💬 Join our Community: Whether you're a user looking for help or a developer wanting to contribute, we'd love to have you in our Discord server! Share feedback, ask questions, discuss new features, or just hang out with fellow privacy enthusiasts.

Features

  • 🔐 Privacy-first — Your data is never shared with third parties. You own it
  • 🏦 Bank account management — Track multiple accounts in one place
  • 📊 Transaction categorization — Automatic and manual categorization
  • 🤖 Automation rules — Set up rules to auto-categorize transactions
  • 📈 Financial insights — Understand your spending patterns

Tech Stack

  • Backend: Laravel 12, PHP 8.4
  • Frontend: React 19, Inertia.js v2, TypeScript
  • Styling: Tailwind CSS v4
  • Database: MySQL
  • Cache/Queue: Redis
  • Testing: Pest v4

Running Locally

The easiest way to get started is using our automated setup script:

bash <(curl -fsSL https://whisper.money/setup.sh)

After installation, just visit https://whisper.money.localhost in your browser.

Manual Setup

If you prefer to set up manually:

  1. Clone the repository:
git clone https://github.com/whisper-money/whisper-money.git
cd whisper-money
  1. Run the setup script:
whispermoney install

Available Commands

Important: You must run whispermoney install before using any other command. If you skip the install step, commands like start will not work.

Once installed, you can use the whispermoney command for common tasks:

# Start all services
whispermoney start

# Stop all services
whispermoney stop

# Upgrade to latest version
whispermoney upgrade

# Interactive menu
whispermoney

Development Server

For active development with hot reloading:

composer run dev

This will concurrently start:

  • PHP development server (via Portless HTTPS proxy)
  • Queue worker
  • Log viewer (Pail)
  • Vite dev server

The application will be available at https://dev.whisper.money.localhost. In git worktrees, the branch name is automatically prepended (e.g. https://fix-ui.dev.whisper.money.localhost).

Running with Docker (Production Image)

For testing the production Docker image locally:

  1. Copy the production environment file:
cp .env.production.example .env
  1. Start the services:
docker compose -f docker-compose.production.yml up -d

The application will be available at http://localhost:8080.

To use a different port, set APP_PORT:

APP_PORT=3000 docker compose -f docker-compose.production.yml up -d

Deploying to Coolify

Whisper Money can be easily deployed to Coolify using our Docker Compose template.

Quick Deploy

  1. In Coolify, create a new resource and select Docker Compose
  2. Choose Empty Compose File as the source
  3. Paste the contents from our template: 👉 whisper-money.yaml
  4. Deploy!

The template includes:

  • Whisper Money application container
  • MySQL 8.0 database with health checks
  • Persistent volumes for data and storage
  • Auto-generated database credentials

Required Environment Variables

Variable Description
RESEND_API_KEY Email service API key (for password resets, notifications)

Note: APP_KEY and APP_URL are auto-configured. The container generates an APP_KEY on first startup if not provided.

Optional Environment Variables

Variable Default Description
DRIP_EMAILS_ENABLED true Enable drip emails (welcome, onboarding, feedback)
REGISTRATION_ENABLED true Set to false to close public sign-ups (the /register routes return a 403 and every registration CTA is hidden) while keeping /login open
SUBSCRIPTIONS_ENABLED false Enable Stripe subscriptions
STRIPE_KEY - Stripe publishable key
STRIPE_SECRET - Stripe secret key
STRIPE_WEBHOOK_SECRET - Stripe webhook signing secret
AI_PROVIDER gemini AI provider for every AI feature (gemini, ollama, openai, ...)

AI Provider

Whisper Money's AI features (transaction categorization and automation-rule suggestions) run on laravel/ai and default to Google Gemini. The provider is configurable independently of the model, so you can point the app at any text provider laravel/ai supportsgemini, openai, anthropic, azure, groq, xai, deepseek, mistral, or a self-hosted Ollama server. Ollama is the headline case because it keeps AI processing fully local and private — data never leaves your infrastructure — but the switch is generic.

Each provider needs its own credentials configured for laravel/ai (e.g. GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, or OLLAMA_URL). An unknown or non-text provider fails fast when the AI feature runs.

Variable Default Description
AI_PROVIDER gemini Provider for all AI features. Set once to switch everything.
AI_SUGGESTIONS_PROVIDER AI_PROVIDER Override the provider for rule suggestions only.
AI_CATEGORIZATION_PROVIDER AI_PROVIDER Override the provider for transaction categorization only.
AI_SUGGESTIONS_MODEL gemini-flash-latest Model used for rule suggestions.
AI_CATEGORIZATION_MODEL gemini-flash-latest Model used for transaction categorization.
GEMINI_API_KEY - Required when the provider is gemini.
OLLAMA_URL http://localhost:11434 Ollama server URL (used when the provider is ollama).
OLLAMA_API_KEY - Optional; only needed behind an authenticating proxy.

Example: fully local AI with Ollama

AI_PROVIDER=ollama
OLLAMA_URL=http://ollama.example.local:11434
AI_SUGGESTIONS_MODEL=gemma3:12b
AI_CATEGORIZATION_MODEL=gemma3:12b

Make sure the model is pulled on the Ollama server first (ollama pull gemma3:12b). Any other provider follows the same pattern: set AI_PROVIDER, that provider's credentials, and the *_MODEL vars to one of its models. Gemini remains the default, so existing deployments are unaffected.

Star History

Star History Chart

License

This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.