A cross-platform expense splitting and group finance app built with Flutter. Split bills, track who owes whom, and settle up on Android, iOS, Web, macOS and Windows from a single codebase.
EquiSplit is serverless: the app talks directly to Firebase (Auth, Firestore, Storage, Cloud Functions, FCM). There is no REST API and no Laravel backend in the request path. The retired Laravel implementation lives in the sibling equisplit repository for historical reference only.
- Create groups for trips, roommates, events or households
- Invite members with a shareable
/join?token=link and claim it on a second account - Member directory with avatars, contact details and per-group roles
- Cash-inflow tracking — money coming in (salary, refunds) linked to specific expenses
- Add expenses with amount, category, date and payer
- Split across any subset of members — equally or by custom shares
- Automatic balance calculation: who owes whom, minimised into a settlement list
- Settlement tracking, so a recorded payment clears the debt
- Spending statistics and trends per group, rendered with FL Chart
- Category breakdowns and cash-flow charts
- Live updates from Firestore snapshots — balances and expenses appear without a refresh
- Push notifications via FCM, including settlement reminders and expense alerts
- Offline-first — Firestore caches reads and queues writes until connectivity returns
- Google sign-in, email/password auth, avatar uploads to Firebase Storage
- Slidable expense rows, emoji picker, currency picker, in-app ads on mobile
| Platform | Status | Notes |
|---|---|---|
| 🌐 Web | ✅ | Firebase JS SDK 13.1.0; web push via service worker |
| 🍎 macOS | ✅ | Native macOS application |
| 🪟 Windows | ✅ | Native Windows application (no local notifications) |
| 🤖 Android | ✅ | minSdk 24 |
| 📱 iOS | ✅ | iOS 13.0+ |
Linux is intentionally unsupported: FlutterFire has no Linux implementation for this stack.
- Firebase project:
equisplit-d4c74✅ configured - Firestore database:
europe-west1✅ deployed (recreated from nam5) - Hosting:
https://equisplit-d4c74.web.app✅ deployed and live - Cloud Functions: ⏳ pending (requires Cloud Functions API enablement)
- Storage: ✅ configured (bucket reference:
equisplit-d4c74.firebasestorage.app)
Note: The Cloud Functions API is currently disabled and requires console enablement. Rules, indexes, and hosting are deployed and live.
Check the live site:
curl -I https://equisplit-d4c74.web.app
# Should return HTTP/2 200Feature-first structure with a shared layer:
lib/
├── main.dart # Entry point (Firebase bootstrap, sign-in init, ads)
├── firebase_options.dart # Generated by flutterfire configure
└── src/
├── app.dart # App shell & theme
├── core/
│ ├── constants/ # Default category list
│ └── firebase/
│ ├── firebase_bootstrap.dart # init + emulators + App Check
│ ├── firestore_refs.dart # typed collection/document refs
│ ├── firestore_converters.dart # Timestamp/num/string converters
│ └── firebase_error_mapper.dart
├── features/
│ ├── auth/ # Firebase Auth, profile, invites, avatar
│ ├── home/ # Dashboard & personal balance
│ ├── groups/ # Group list, detail, invites
│ ├── expenses/ # Expenses, splits, settlement, balances
│ ├── categories/ # Category management
│ ├── cash_inflows/ # Income / cash-inflow tracking
│ ├── people/ # Member directory
│ └── analytics/ # Statistics & charts
└── shared/
├── services/ # FCM/local notifications
├── presentation/widgets/ # Reusable UI widgets
└── utils/ # Formatters, platform detection, helpers
functions/ # Cloud Functions (TypeScript, Node 20)
├── src/auth.ts # bootstrapUser, syncMemberProfile
├── src/groups.ts # invites, members, delete, reset
├── src/expenses.ts # onExpenseCreated push
├── src/notifications.ts # reminders, test push
├── src/devices.ts # hasFcmToken mirrors
├── src/cleanup.ts # scheduled invites + onUserDelete
└── src/lib/ # Admin SDK helpers, FCM, constants
firestore.rules # Security rules
firestore.indexes.json # Composite + collection-group indexes
storage.rules # Avatar-only bucket rules
firebase.json # Rules/indexes/storage/functions/emulators
State management: Riverpod. Repositories expose Streams over Firestore, and providers are scoped per feature.
Balance math: BalanceCalculator is pure Dart and runs in the client (features/expenses/domain). There is no users.balance field; the personal balance is derived as cash inflows − personal-group expenses.
Routing: Navigator + MaterialApp.home; invite links are read from Uri.base.queryParameters['token'] in AuthGate.
| Concern | Choice |
|---|---|
| Framework | Flutter 3.47 / Dart 3.13+ |
| State management | Riverpod |
| Backend | Firebase Auth, Firestore, Storage, Cloud Functions, FCM |
| Auth | firebase_auth, google_sign_in 7 |
| Push | firebase_messaging 17 + flutter_local_notifications |
| Security | Firestore/Storage rules, optional App Check |
| Charts | FL Chart |
| UI | Google Fonts, flutter_slidable, emoji_picker_flutter, currency_picker |
| Ads | google_mobile_ads (Android/iOS only) |
| Media | image_picker |
- Flutter SDK 3.47 or higher
- Dart 3.13 or higher
- Node.js 20 and
firebase-tools(npm i -g firebase-tools) for Functions and emulators - JDK 21+ —
firebase-toolsrefuses to start the emulators on older runtimes - Platform toolchains: Xcode + CocoaPods (macOS/iOS), Android Studio/SDK, Visual Studio 2022 (Windows)
The Firebase project is equisplit-d4c74. Its client config is already committed in lib/firebase_options.dart, android/app/google-services.json, and the Apple plists.
git clone https://github.com/zuhaibranaa/expense_split.git
cd expense_split
flutter pub get
npm --prefix functions installStart the emulator suite (Auth, Firestore, Storage, Functions, UI):
firebase emulators:startThen launch the app with the emulator flag:
flutter run -d chrome --dart-define=USE_FIREBASE_EMULATORS=trueThe Android emulator automatically uses 10.0.2.2 for the host; every other target uses localhost.
flutter devices
flutter run -d chrome
flutter run -d macos
flutter run -d windows
flutter run -d <android-or-ios-device>Already deployed in this session:
# Rules, indexes, and hosting are already deployed:
firebase deploy --only firestore:rules,firestore:indexes,storage
firebase deploy --only hostingCurrent status:
- ✅ Firestore rules and indexes deployed to
europe-west1 - ✅ Hosting live at
https://equisplit-d4c74.web.app - ⏳ Cloud Functions pending (requires Cloud Functions API enablement)
To enable Cloud Functions API:
- Go to Firebase Console
- Enable the Cloud Functions API
- Set up Blaze billing if needed
- Then run:
npm --prefix functions run build
firebase deploy --only functionsFunctions deploy to europe-west1 (see functions/src/index.ts). Set the invite link host if you use a custom domain:
# functions/.env
INVITE_BASE_URL=https://app.example.comWithout functions/.env, invite links default to https://equisplit-d4c74.web.app.
Client wiring is compiled out unless you pass:
flutter run -d chrome --dart-define=ENABLE_APP_CHECK=trueDo not enforce App Check in Firebase until parity QA passes.
flutter build web --release # build/web/
flutter build apk --release # build/app/outputs/flutter-apk/app-release.apk
flutter build appbundle --release # build/app/outputs/bundle/release/
flutter build ipa --release # requires Xcode signing
flutter build macos --release
flutter build windows --releaseFor web hosting on Firebase:
flutter build web --release
firebase deploy --only hostingNote: The Flutter web build and Firebase Hosting deployment has already been completed in this session. The site is currently live at https://equisplit-d4c74.web.app.
flutter analyze # static analysis
flutter test # 33 unit tests (converters, models, balances, errors)
dart format lib test
npm --prefix functions run build # type-check Cloud Functions
npm --prefix functions run lintCurrently deployed:
✅ Rules & Indexes: Deployed to production Firestore (europe-west1)
✅ Hosting: Live at https://equisplit-d4c74.web.app
⏳ Cloud Functions: Pending (requires Cloud Functions API enablement)
Emulator testing (for development):
# Firestore + Storage rules (36 tests, every deny path)
firebase emulators:exec --only firestore,storage \
"npm --prefix functions test"
# Golden path: signup → personal group → invite → claim → expense → reminders
# → reset → remove member → delete group
firebase emulators:exec --only auth,firestore,functions \
"npm --prefix functions run test:integration"The golden-path test talks to the emulator REST APIs directly, so the real rules and callables are exercised without the Flutter client.
Web
auth/unauthorized-domain→ add the origin under Firebase Auth → Settings → Authorized domains- Push does nothing → confirm
web/firebase-messaging-sw.jsis served from the site root and the VAPID key matchesNotificationService._vapidKey
macOS / iOS
- CocoaPods errors →
pod installinside the platform directory - Google sign-in fails → check the
REVERSED_CLIENT_IDURL scheme is inInfo.plist
Android
- Emulator can't reach emulators →
10.0.2.2is handled automatically byfirebase_bootstrap.dart; verify the emulator is started withfirebase emulators:start google-services.jsonmissing → re-runflutterfire configure --project=equisplit-d4c74
Functions (pending)
permission-deniedcalling a function → send a CORS-enabled call from the app (callables do this) and check the user is signed ininternalerrors →firebase functions:log
Current status
- Hosting is deployed and live:
https://equisplit-d4c74.web.app - Cloud Functions API requires console enablement (not yet deployed)
General
- Build errors →
flutter clean && flutter pub get - Platform unavailable →
flutter doctor
- Migration contract —
decisions.md - Agent implementation guide —
AGENTS.md - Flutter docs — docs.flutter.dev
- FlutterFire — firebase.flutter.dev
- Riverpod — riverpod.dev
MIT