Skip to content
jpaulos.dev

MoneyLens

A personal finance app, built end to end and used for real, with statement parsing, spending forecasts and alerts.

2026 · 5 min read
  • Python
  • FastAPI
  • PostgreSQL
  • React
Code on GitHub (opens in a new tab)
On this page
App tour: all data is made up.

The problem

Our household finances started in a spreadsheet. It worked for writing down expenses, but not for everything else: credit card bills with installments spanning several months, recurring bills that need to be confirmed, statements from different banks and the question that matters most in the middle of the month: how much can I still spend?

Every bill was typed in by hand, and a typo only showed up at the end of the month. MoneyLens replaces that typing with automatic parsing, with checks, and answers that question with numbers.

What the app does

  • Monthly dashboard: income, spending, projected balance, a 50/30/20 budget and what is still waiting for confirmation.
  • Transactions, installments and recurring bills: installment purchases by card, Pix or bank slip create the future installments; recurring bills come in as expected and are confirmed with one click.
  • PDF import: Inter credit card bills and Nubank and PicPay bank statements. The app checks the sum against the printed totals, recognizes categories and keeps anything it does not recognize in standby. When an item is resolved with “remember”, it learns the rule.
  • Cards: each month’s bill, the limit in use and payments.
  • Forecast: how much each category should end the month at, with a likely amount and a likely maximum.
  • Spending limits: by amount or by percentage of income, with alerts at 50%, 90% and 100% and a suggestion based on history.
  • Multiple users: each person only sees their own data.
Monthly dashboardMonthly dashboard
Monthly dashboard
TransactionsTransactions
Transactions
Cards and billsCards and bills
Cards and bills
Reviewing an imported billReviewing an imported bill
Reviewing an imported bill
Forecast per categoryForecast per category
Forecast per category
Spending limitsSpending limits
Spending limits

Architecture

The front-end is a React app served by a Cloudflare Worker, which also forwards API calls to the back-end. The FastAPI API runs in a container on Render and stores everything in PostgreSQL on Supabase.

IN PRODUCTIONBrowserReact + TanStack QueryCloudflareWorker + static siteFastAPI APIon RenderPostgreSQLSupabase, with RLSFORECASTLocal trainingPyTorchONNX modelstored in the databaseForecastonnxruntime in the API

The forecast takes a separate path: training runs off the server, in PyTorch, and the model exported as ONNX goes into the database, one per user. In the API, the model runs on onnxruntime.

Technical decisions

Decision

PyTorch only for training; the API runs the model as ONNX

The production image does not install PyTorch. The model is trained off the server, exported to ONNX and stored in the database; the API only loads and runs it. The container stays small and forecasts stay fast.

Decision

PDF parsers that check the totals

Each parser adds up what it read and compares it with the totals the bank itself prints: the bill total, each day’s balance, the closing balance. If they do not match, nothing is imported. Rejecting a PDF is better than silently saving a wrong number.

Decision

User isolation and a database closed to the public

Every query filters by owner, and a test checks that no API route is left out: another person’s item responds as if it did not exist. On Supabase, RLS is enabled with no policies and the public API roles lose access, so the database only serves the back-end.

Decision

A published migration is never edited

The app is deployed on every push to main, and migrations run at startup. A migration that has reached production is never changed: any change becomes a new migration, tested on SQLite and PostgreSQL.

Decision

One time zone for the whole test run

An end-to-end test failed only in CI, between 9 p.m. and midnight: the server was on UTC, the browser on São Paulo time, and they disagreed about what day it was. Now the test server and the browser run in the same time zone, São Paulo’s.

Quality and testing

back-end tests
550
front-end tests
196
end-to-end tests
33
databases in CI
2
  • Back-end: Pytest covers business rules, the API, security, migrations and the PDF parsers. In CI, the suite runs on SQLite and PostgreSQL.
  • Front-end: Vitest for forms, utilities and the Cloudflare Worker.
  • End to end: Playwright starts the app with a temporary database and goes through the flows on desktop and mobile.
  • Real data never gets in: the bills and statements used in tests are synthetic, generated by code.

What I learned

  • Checking is cheaper than fixing. The “only import if the total matches” rule prevented more problems than any manual review.
  • A model needs a yardstick. Every training run reports the neural network’s error next to the error of a simple statistic, on the same test month. With that reference, it is clear when the model actually helps.
  • Time zones are a requirement. Dates show up in bills, installments and alerts; handling the time zone from day one avoids bugs that only appear at night.
  • End-to-end tests pay for themselves. They caught problems no unit test would, like a click landing on the previous page during a route change.