A money tracker that keeps working when the network doesn’t.
Android app · Open source

- Offline queuesurvives restarts, retries with backoff
- clientIdidempotent sync, no duplicate expenses
- AES-256-GCMexpense details encrypted at rest
- On-device MLNaive Bayes category suggestions
Why I built it
the problem, minus the buzzwordsExpense apps that need the network to save a transaction lose entries exactly when you’re out spending: in shops, on trains, in basements. And the naive fix, “retry later”, creates a new problem: the same expense saved twice, and balances that slowly drift.
It also had to handle a second kind of user: an admin who creates accounts for other people, assigns them balances and sees where the money went.
How it works
write on the phone, settle on the server- Local write1
The expense is saved on the device and shown at once, tagged with a client-generated ID.
#device
- Offline queue2
Persisted in AsyncStorage, survives app restarts, retries 5xx / 408 / 429 with backoff and jitter.
#queue
- Express API3
A unique index on clientId turns a retried request into a no-op instead of a duplicate.
#idempotent
- Transaction4
Expense, account balance and running balance are written together in one MongoDB transaction.
#consistent
- Reports5
Monthly stats are cached per user; PDF reports are built with PDFKit and emailed through Brevo.
#output
- +If a queued expense points at an account deleted in the meantime, it falls back to the Primary account instead of failing the sync.
- +Google Sign-In and email login, short-lived access tokens with rotating refresh tokens kept in Secure Store.
- +The backend tells the app about updates: OTA, APK or Play Store, optional or forced.
Built with React Native, Expo Router, TypeScript, Zustand, Node.js, Express 5, MongoDB, PDFKit, Brevo.
Decisions I made
and why. mostly why.1.Make retries harmless
Offline sync means the same request can arrive twice. Every expense carries a clientId with a unique index behind it, so the server can accept retries forever without creating duplicates.
2.Store the running balance
Each transaction saves the balance after it (afterBalance). The app can draw balance history immediately instead of replaying every past transaction.
3.Refresh the token once
When the access token expires, only one refresh request goes out. Every other request waits for it and then retries, and the user is logged out only if the refresh itself fails.
4.Guess categories on the phone, carefully
A small Naive Bayes model bundled with the app suggests a category as you type, even offline. When it isn’t confident it says “Other” instead of guessing.
5.Encrypt what’s personal
Expense details are encrypted with AES-256-GCM in a schema setter, so controllers never deal with it. An idempotent migration encrypted old rows while the app stayed live.



