Group expense splitting and settle-up app. Built with Flutter, Riverpod, GoRouter, and Supabase.
- Groups β Create groups (trips/events) with participants.
- Expenses β Log multi-currency expenses with payer and split type (Equal, Parts, Amounts).
- Balance β View who is owed / who owes; settle-up suggests minimal transfers.
- Record Settlement β Tap a settlement suggestion to record the payment and zero out the debt.
- Settings β Theme, language, and Local Only toggle.
- Offline-first β Works entirely offline via local SQLite. Syncs directly with Supabase when online.
| Mode | What works | Data location |
|---|---|---|
| Local-Only (default) | Everything β full CRUD, settlement, no restrictions | Local SQLite only |
| Online (with Supabase) | Everything + invites, members, cross-device sync | Supabase + local SQLite cache |
When in Online mode and temporarily offline, you can still add expenses (queued for later sync). Other features like invites and member management require connectivity.
The app is deployed as a Progressive Web App (PWA) and is hosted on Firebase Hosting. New versions are deployed via the Release workflow (on version tags v* or manual run). It works locally and offline.
- Install: Add to Home Screen via Chrome, Edge, or Safari.
- Offline: Works perfectly without an internet connection.
Option 1: Obtainium (Recommended) - Automatic Updates
- Manual Setup: Install Obtainium from F-Droid or GitHub Releases, then:
- Open Obtainium and tap the "+" button
- Select "GitHub Releases" as the source
- Enter repository:
Zyzto/Hisab
- Obtainium will automatically track new releases and notify you of updates
Option 2: Google Play Store WIP
- Available on Google Play Store (when published)
- Automatic updates through Play Store
Option 3: Direct Install (APK)
- Download the APK (e.g.
app-release.apk) from GitHub Releases - Enable "Install from unknown sources" in your Android settings
- Tap the downloaded APK file to install
- Flutter SDK ^3.10.0
- Dart ^3.10.0
flutter pub get
dart run build_runner build --delete-conflicting-outputs
flutter runWithout --dart-define parameters the app runs in local-only mode (no sign-in, no sync).
flutter run \
--dart-define=SUPABASE_URL=https://xxxxx.supabase.co \
--dart-define=SUPABASE_ANON_KEY=eyJhbGci...For web: ensure web/sqlite3.wasm is present (it is generated locally and typically not committed). Generate it with:
dart run powersync:setup_web- State β Riverpod 3 with
riverpod_annotationcodegen. - Navigation β GoRouter with ShellRoute and bottom nav.
- Data β Repository pattern:
IGroupRepository,IParticipantRepository,IExpenseRepository.- Local SQLite (via PowerSync package) is the single local database engine.
- When online, writes go to Supabase first, then update local cache.
- Reads always come from local SQLite for speed and reactivity.
- Complex operations (invite accept, ownership transfer, etc.) use Supabase RPC functions.
- Sync β
DataSyncServicehandles: full fetch from Supabase, push pending offline writes, periodic refresh. - Auth β Supabase Auth (email/password, magic link, Google OAuth, GitHub OAuth).
- Domain β
lib/domain/: Group, Participant, Expense (amounts in cents), SplitType, SettlementTransaction, and related types (e.g. GroupMember, GroupInvite, SettlementMethod, SettlementSnapshot).
For online mode, set up Supabase. See SUPABASE_SETUP.md for the full step-by-step guide covering:
- Creating the Supabase project and applying database migrations
- Configuring authentication providers (email, Google, GitHub)
- Deploying Edge Functions (the repo includes
invite-redirect;telemetryandsend-notificationare documented in setup docs and can be deployed from those definitions) - Configuring
--dart-defineparameters
If no --dart-define values are provided, the app runs in local-only mode β all features work except sign-in and cross-device sync.
| Issue | Quick fix |
|---|---|
| App shows local-only mode | Ensure both --dart-define params are set |
| SQLite web crash | Run dart run powersync:setup_web to download WASM |
| OAuth redirect fails | Check Supabase Auth redirect URLs match your app |
| Migration fails | Ensure stable internet; migration is idempotent |
Full configuration reference: CONFIGURATION.md.
All secrets are provided at build time via --dart-define β nothing is committed to the repository. The only gitignored file for local secrets is lib/core/constants/app_secrets.dart (copy from app_secrets_example.dart); it holds the report-issue URL. All Supabase and Firebase values are provided via --dart-define only.
The project includes a release workflow (.github/workflows/release.yml) that builds Android APK/AAB, deploys to Google Play, and deploys the web app to Firebase Hosting. It triggers on version tags (v*) or manual dispatch.
Go to repo Settings β Secrets and variables β Actions and add each secret.
| Secret | How to get it |
|---|---|
SUPABASE_URL |
Supabase Dashboard β Settings β API β Project URL |
SUPABASE_ANON_KEY |
Supabase Dashboard β Settings β API β anon public key |
SITE_URL |
Your web app URL, e.g. https://hisab.shenepoy.com |
| Secret | How to get it |
|---|---|
FIREBASE_SERVICE_ACCOUNT |
Firebase Console β Project Settings β Service accounts β Generate new private key β paste the entire JSON |
| Secret | How to get it |
|---|---|
FCM_VAPID_KEY |
Firebase Console β Project Settings β Cloud Messaging β Web Push certificates β generate or copy the key pair's public key (required for web push token) |
Generate a keystore (one-time setup):
keytool -genkey -v \
-keystore ~/hisab-release.jks \
-keyalg RSA -keysize 2048 \
-validity 10000 \
-alias hisab-releaseYou will be prompted for a store password and a key password (press Enter at the key password prompt to reuse the store password).
Then base64-encode it:
base64 -w 0 ~/hisab-release.jksCopy the keystore into the project for local signed builds (already gitignored):
cp ~/hisab-release.jks android/app/release-keystore.jks| Secret | Value |
|---|---|
KEYSTORE_BASE64 |
Output of base64 -w 0 ~/hisab-release.jks (entire string, no newlines) |
KEYSTORE_PASSWORD |
The store password you chose during keytool |
KEY_ALIAS |
hisab-release |
KEY_PASSWORD |
The key password (same as store password if you pressed Enter) |
GOOGLE_SERVICES_JSON |
Base64-encoded android/app/google-services.json (Firebase Console β Project settings β your Android app β download google-services.json, then base64 -w 0 android/app/google-services.json) |
| Secret | How to get it |
|---|---|
PLAY_STORE_SERVICE_ACCOUNT_JSON |
Google Play Console β Setup β API access β create/link a service account β download JSON key |
Create android/key.properties (gitignored):
storeFile=/absolute/path/to/android/app/release-keystore.jks
storePassword=your_store_password
keyAlias=hisab-release
keyPassword=your_key_password- Change password modal β focus loss on text fields: The change-password sheet (Settings β Change password) can lose focus on the current/new/confirm password fields at random when clicking (e.g. on the visibility toggles or elsewhere). In-app controls were made non-focusable (
canRequestFocus: false) to reduce this, but the issue may still occur on some platforms or with certain focus/route updates. Workaround: tap directly on the text field again to refocus.
This project is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
- You may share and adapt the material with attribution, for non-commercial use only, and you must share adaptations under the same license.
- Full legal text: LICENSE in this repo, or legalcode on the CC site.
- Human-readable summary: CC BY-NC-SA 4.0.







