Skip to content
Back to portfolio

Garzoni

Live on web, iOS and Android

AI-powered financial learning platform

Garzoni teaches personal finance the way a language app teaches vocabulary — ten-minute lessons, daily streaks, spaced repetition, and AI tools that let you rehearse a money decision before you make it. I designed, built, shipped and now run all of it: the API, the web app, the mobile app, the billing, the infrastructure and the content pipeline.

Role
Founder & sole engineer
Duration
21 months, ongoing
Surfaces
Web, iOS, Android
Scale
~250k lines · 900+ commits
Users
150+ on web, iOS and Android
Backend
Django 5.2 · DRF · Celery
Clients
React 19 · Expo
01 / Constraints

What made it hard.

One engineer, three surfaces

Web, iOS and Android had to stay in lockstep with no team to divide them between. Anything duplicated across clients was going to drift, so the architecture had to make sharing the default rather than a discipline.

Real money, three payment rails

Subscriptions sell through Stripe on web, StoreKit on iOS and Play Billing on Android. A user who subscribes on their phone must be entitled on the web the moment they log in — from three systems that disagree about what a subscription even is.

LLM cost scales with users, not revenue

An AI tutor, voice tutoring, vision-based receipt parsing and a daily re-ranked learning path all bill per call. On a free tier, naive implementations lose money linearly with signups.

Store review is a hard gate

Apple and Google review every release. Financial-education content invites extra scrutiny, so the compliance position — education, not advice; no bank execution; no custody — had to be designed in, not bolted on.

02 / Architecture

One repo, one core.

Garzoni system architectureA React 19 web app and an Expo mobile app both depend on a shared TypeScript core, which talks to a Django 5.2 DRF API. The API is backed by Postgres with pgvector, Redis with Celery, and third-party services: OpenAI, RevenueCat and Plaid.React 19 web appVercel · Cloudflare edgeExpo mobile appiOS + Android · EAS releasesShared TypeScript coretypes · API client · validation · design tokensDjango 5.2 / DRF APIRailway · Docker · health-gated deploysPostgres + pgvectorrelational + embeddingsRedis + Celeryqueues · scheduled jobsOpenAI · RevenueCat · Plaidtutor · entitlements · banking
One repository, one set of types. Both clients depend on the same core, so an API change fails at compile time in every consumer rather than at runtime on someone's phone.
03 / Decisions

Choices, and what they cost.

Every one of these bought something and gave something up. The trade-off is the interesting half.

A pnpm monorepo with a shared TypeScript core

Problem

Two clients on different runtimes (browser and React Native) needed identical domain types, API contracts, validation and design tokens.

Approach

One repository: a Django API, a React 19 web app, an Expo mobile app, and a TypeScript core package that both clients depend on. Types, API client, formatting and design tokens are defined once and consumed everywhere.

Trade-off

Builds and CI are heavier than three small repos, and a careless change in core can break both clients at once. In exchange, an API change surfaces as a type error in every consumer at compile time instead of as a bug report from a phone.

Function calling instead of free-form generation

Problem

A tutor that answers in prose is impossible to verify, easy to hallucinate with, and cannot drive the app's own UI.

Approach

The tutor is built on OpenAI function calling. The model chooses from a fixed set of tools — look up a lesson, run a budget calculation, open a simulator — and the app executes them. Retrieval runs over the curriculum itself with pgvector, so answers cite material that actually exists.

Trade-off

It constrains what the tutor can say, and every new capability means defining a new tool rather than editing a prompt. That constraint is the point: the model routes, the application computes, and any number it quotes came from real code.

Hash short-circuiting the daily re-rank

Problem

The learning path is re-ranked daily by an LLM. Most users' inputs do not change between days, so most of those calls would pay to produce a result identical to yesterday's.

Approach

The inputs that determine ranking are hashed. If the hash matches the last run, the previous ranking is reused and no model call is made.

Trade-off

Ranking can lag a change until the next cycle, and the hash has to cover every genuine input or results go stale. The cost curve flattens against inactive users, which is most of any free tier.

RevenueCat as the single entitlement authority

Problem

Stripe, App Store and Play each model subscriptions differently. Reconciling three webhook formats, three renewal semantics and three refund flows into one answer to 'what is this user entitled to?' is its own product.

Approach

All three rails feed RevenueCat, and the backend asks one question — what is this user entitled to — then enforces per-tier quotas and paywalls behind that single answer.

Trade-off

A third party sits on the revenue path and takes a cut. Worth it: cross-platform entitlement is a notorious source of silent, revenue-losing bugs, and it is not where a solo engineer should be spending their originality.

Health-gated deploys and automated backups

Problem

With no one else on call, a bad deploy at the wrong moment is an outage that lasts until I happen to look at my phone.

Approach

Deploys are gated on health checks, backups run automatically, Sentry reports errors, and CI enforces pinned dependencies, vulnerability auditing and secret scanning before anything ships.

Trade-off

Slower releases and occasional false-positive rollbacks. Preferable to discovering a migration failure from a user's review on the App Store.

04 / Platform

The unglamorous half.

What turns a demo into something people can pay for and keep using.

Engagement

  • Streaks, daily and weekly missions
  • Leagues and leaderboards
  • Spaced-repetition review
  • XP and progression

Identity & safety

  • JWT auth, Google and Apple sign-in
  • Brute-force protection
  • reCAPTCHA Enterprise
  • Sentry error tracking

Money features

  • Plaid open-banking budgeting
  • Statement import
  • Receipt analysis via GPT-4o vision
  • Per-tier quotas and paywalls

Reach

  • EN/RO localisation
  • Shared design tokens across web and mobile
  • Cloudflare edge caching
  • 20+ backend test suites
05 / Retrospective

What I would do differently.

  • 01

    The monorepo earned itself back within weeks, but I set it up after the second client already existed. Doing that first would have saved a painful migration of duplicated types.

  • 02

    I under-invested in the content pipeline early. Lessons started as code, and moving them to data later cost more than authoring them as data from the start would have.

  • 03

    Cost controls like hash short-circuiting were retrofitted once the bill made them obvious. On the next AI feature I would budget the per-call cost during design, alongside latency.