A free expense-splitting web app — a more generous Splitwise alternative. Flutter web + Firebase. Multi-currency, unlimited expenses. Tracks who owes whom across personal balances and groups; it does not move money.
The expense ledger is the source of truth. A Cloud Function recomputes a
per-group balances summary on every expense write — and is the only
writer of that summary (security rules forbid clients from writing it), which
keeps the debt graph tamper-proof. Clients read the cheap summary for display.
Simplify Debts runs client-side, display-only, and never mutates data. Money
is stored in each expense's original currency + a pivot rate (→USD) captured
at entry time; original amounts are never mutated, so single-currency users
see exact figures and only blended cross-currency totals incur (acknowledged)
rounding.
See docs/DATA_MODEL.md for the full Firestore layout.
Works the same whether the container is opened via GitHub Codespaces or locally
(VS Code Dev Containers on Docker Desktop / WSL2). The dev container
(.devcontainer/) auto-installs Flutter (stable, web only), the Firebase
CLI, a JRE for the emulators, and Node, then pre-downloads the emulators. That
takes a few minutes on first create. The emulators auto-start on every
container start (postStartCommand), with data persisted in
emulator-data/ so test accounts survive restarts.
./scripts/web.sh # run the app (forwarded port 3000)
(cd functions && npm run build:watch) # only while editing Cloud FunctionsOpen the forwarded port 3000 in your browser. Debug builds auto-connect to the emulators; the Emulator Suite UI is on port 4000.
Google sign-in in development goes through the Auth emulator, which fakes the account chooser — no real OAuth setup, no google-services.json, no client IDs.
A popup that opens but never loads means the emulators aren't running (they
should have auto-started). Check /tmp/emulators.log, or start them by hand:
./scripts/emulators.shThe app also preflights the Auth emulator before opening the popup and shows a
clear error instead of hanging (see auth_service.dart).
flutter test.devcontainer/ Dev container image + auto-setup + emulator auto-start
functions/ Cloud Functions (TypeScript) — balance pipeline
lib/
firebase_bootstrap.dart Firebase init; auto-wires emulators in debug
firebase_options.dart Web Firebase config (public identifiers, not secrets)
auth_service.dart Google sign-in via signInWithPopup + profile doc
scripts/
web.sh Run the web app on port 3000
emulators.sh Start emulators with data persistence
firestore.rules DEFAULT-DENY security rules (balances doc client-locked)
firebase.json Emulator ports + rules/functions/hosting wiring
docs/DATA_MODEL.md Firestore collection structure
Android/iOS were deliberately dropped — this is a web app. If you ever revive
them, start from git log (the android/ tree and its docs were removed in the
"web-only" cleanup commit).
Rules are default-deny. The single most important invariant: clients can
read groups/{id}/balances/* but can never write it. Only the Cloud
Function (admin SDK) writes balances. Test rule changes against the emulator
before deploying.
flutter build web
firebase deploy # hosting + functions + rulesDeploying to real Firebase (not the emulator) is the point where Google sign-in needs real OAuth: enable the Google provider in Firebase console → Authentication → Sign-in method, and add your hosting domain to the authorized domains list. Nothing in the app code changes.