MoneyLens
A personal finance app, built end to end and used for real, with statement parsing, spending forecasts and alerts.
On this page
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.












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