Skip to content
All projects

Mobile product · AI · Sync

Finlens — Personal Finance App

A personal finance app I designed and built on my own: Flutter app, Fastify backend and AI features on Gemini — voice-to-transaction, a finance assistant chat and a voice copilot for the app's forms.

My role
Independent product: idea, design, Flutter app, backend, AI integration and deployment.
Stack
  • Flutter
  • Node.js
  • Fastify
  • PostgreSQL
  • Redis
  • Gemini API

The problem

Entering expenses by hand is tedious, families want to share one budget across several phones, and the app has to work offline. That needs reliable sync on the backend — and AI that turns free speech in several languages into correct structured data.

Architecture

Finlens — Personal Finance App — Architecture diagramFlutter appoffline-firstFastify APIsync · auth · AIRedisrate limitsPostgreSQLrecords · changesGoogle Sign-Inid tokensGeminiJSON · SSEEval suite5 languages
The Flutter app keeps data locally and syncs through a Fastify API on PostgreSQL: one call pushes local changes with their versions and pulls everything after the client's cursor. AI requests go through the backend, which keeps the Gemini key, validates every model output against the app's catalog and schema, streams chat replies over SSE and rate-limits each AI feature per user in Redis. Deployed with Docker and PM2 behind nginx on AWS EC2.

Key decisions

  1. 01Optimistic concurrency with a global change cursor

    Why
    Every record carries a version; a change based on an old version comes back as a conflict for the user to resolve. A global sequence orders all changes, and the group row is locked during sync, so no device ever skips a change.
    Trade-off
    Sync within one family group is serialized.
  2. 02Never trust model output

    Why
    Everything Gemini returns is checked against a JSON schema and against the user's own accounts and categories. Invalid parts are dropped instead of failing the whole request.
  3. 03Resilient AI calls

    Why
    When the model is overloaded, the backend retries with backoff and then switches to a fallback model, with per-request timeouts — users get an answer instead of an error.
    Trade-off
    A fallback model may answer slightly worse than the primary one.
  4. 04Measure the AI instead of guessing

    Why
    A held-out set of thousands of voice phrases in five languages, with repeat runs to check stability, shows whether a prompt or model change actually helps before it ships.

What I built

  • Voice → transaction: audio and the user's own catalog in, schema-validated JSON draft out
  • Finance assistant chat streamed over SSE; voice copilot that fills about 20 app forms
  • Offline-first family sync: record versions, cursor-paged change feed, group row locking
  • Google sign-in with revocable JWTs, Play-compliant account deletion and automatic data expiry
  • LLM evaluation suite: about 7,000 cases per language in 5 languages, plus an app↔server contract checker
  • 254 backend tests on in-memory PostgreSQL (PGlite)

In numbers

voice-parsing eval cases per language
~7,000
languages in the AI evaluation suite
5
automated backend tests
254