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