← back to all projects
project no. 5

A money tracker that keeps working when the network doesn’t.

Android app · Open source

ExpenseGauge screenshot
works with zero signal
  • Offline queue
    survives restarts, retries with backoff
  • clientId
    idempotent sync, no duplicate expenses
  • AES-256-GCM
    expense details encrypted at rest
  • On-device ML
    Naive Bayes category suggestions

Why I built it

the problem, minus the buzzwords

Expense 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
  1. Local write1

    The expense is saved on the device and shown at once, tagged with a client-generated ID.

    #device

  2. Offline queue2

    Persisted in AsyncStorage, survives app restarts, retries 5xx / 408 / 429 with backoff and jitter.

    #queue

  3. Express API3

    A unique index on clientId turns a retried request into a no-op instead of a duplicate.

    #idempotent

  4. Transaction4

    Expense, account balance and running balance are written together in one MongoDB transaction.

    #consistent

  5. 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. 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. 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. 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. 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. 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.

Screens

proof it exists
ExpenseGauge home screen
ExpenseGauge transactions
ExpenseGauge analytics
ExpenseGauge accounts
next up (you’ve come this far)
Memory Dump →
A private memory library that never confuses an AI guess with a fact.