MoneyMate — offline-first personal finance for Bangladesh
An offline-first expense tracker built for Bangladesh: track every taka, set budgets, and settle lends and debts — without an account, a bank link, or an internet connection. MoneyMate records income, expenses, lends, debts and family contributions, groups them under user-defined categories, and turns them into monthly budgets, charts and PDF reports. The defining decision is that the app is useful with nothing switched on: in local-only mode there is no sign-up, no email address, no phone number and no network traffic at all, because every record lives in a SQLite database on the phone. Sync is something you opt into — a spreadsheet in your own Google Drive, the MoneyMate API, or both together — so reads always come from SQLite, writes always land in SQLite first, and the network stays a background detail that can fail without the user noticing. It is built in Flutter with Riverpod and Drift over a FastAPI and PostgreSQL backend, with native home-screen widgets in Kotlin Glance and SwiftUI WidgetKit, five themes in light and dark, and five languages including right-to-left Arabic.

A tour of the build.
Swipe or click through the screens.












Offline-first sync has to survive a bad network, not just a good one.
Offline-first sync has to survive a bad network, not just a good one. The hard part was never sending the data, it was replaying it safely: a client that retries after a timeout must not create duplicates, two devices editing the same record have to converge, and auto-increment integers cannot work when two offline phones both create record #7. The widgets hit a different wall — Android's Glance and iOS's WidgetKit both render outside the app process and neither can show a text field, so the fastest way to log an expense had nowhere to type. And a recurring "logged out for no reason" bug hid inside the API returning 401s in a success-shaped response envelope, so the failure never surfaced as a transport error, the refresh-and-retry interceptor never ran, and a merely expired access token was indistinguishable from a revoked session.
What we did
Every mutation writes to SQLite and enqueues a row in a local sync queue that a background service replays when connectivity returns; replay is idempotent, last-write-wins resolves on the server by updated_at rather than by whoever arrives last, and the delta feed was fixed so a record modified mid-pagination can no longer slip through the gap between pages. Every table uses a client-generated UUID as its primary key, so a row created on a plane keeps its identity when it finally reaches the server. Quick-entry widgets launch a translucent single-purpose activity that looks like a sheet over the home screen and writes through a repository shared with the widget refresh worker, so adding an expense never means waiting for the full app to start. Fixing the response-envelope mapping restored automatic token refresh and removed a whole class of "my data stopped syncing" reports. Arabic support moved every direction-sensitive value to start and end rather than left and right, and amounts run through a locale-aware formatter because Bangladeshi digit grouping is lakh and crore, not thousands.
What changed after
Live on Google Play worldwide since 21 August 2026 — free, with no ads, no subscription and no paywalled features. There is no analytics SDK inside the app, and local-only mode collects nothing at all unless a sync mode is deliberately switched on. Getting there meant closed testing, a production access application, an IARC content rating, a data safety declaration, and a Play requirement that users be able to request account deletion from the web without installing the app — which is why the marketing site carries a full deletion flow with an operator queue behind it.