Context
Vatrio is a receipt and mileage tracker for people who claim work expenses from SARS. You photograph a till slip and get a dated, categorised expense record. A mileage logbook and tax-season exports sit alongside it. I started it in September 2025 and built most of it by the end of that year, then came back in September 2026 to harden it for release. It ships as a PWA and is in private beta. It is the first full-stack product I've taken live on my own.
My role
I'm the sole developer. I designed, built and deployed all of it: the Next.js app, the Supabase database and its access rules, the Gemini extraction, the background jobs and the exports. I didn't make up the requirements alone. I gathered user stories from real users, the people who would be keeping these records, and built from those. I also planned and ran the user test described under Outcome.
Problem
Anyone who claims expenses at tax time ends up with a pile of till slips. Getting them into a spreadsheet means typing each one by hand: date, supplier, amount, VAT and category. In my user test that took about a minute per slip. The date was often the slow part. On 6 of the 10 slips, finding the date took users the most time, because every shop prints it in a different place and format. After that, the rows still have to be sorted and grouped before an accountant can use them.
Constraints
- One developer. Everything had to be something I could build and keep running alone.
- A free AI tier at first. Early on, Vatrio ran on Gemini's free tier. Its limits meant one slip could take up to 30 s to come back. Free-tier data may also be used by Google, which matters when receipts carry client names under POPIA.
- Serverless hosting. The app runs on Vercel, where each request has a time limit, so slow AI calls and large exports can't just sit inside one request.
- Phones first. Users capture slips on their personal phones, since corporate laptops often block websites that haven't been approved.
- SARS rules. Entertainment and gift expenses need a client and a business reason, and the logbook has to be one SARS will accept.
Approach and architecture
The app is Next.js with TypeScript, Tailwind CSS and shadcn/ui, with TanStack Query handling data on the client. Supabase provides auth and Postgres, with row-level security. Receipt photos go straight from the phone to Cloudflare R2 object storage through a signed upload link, so the app server never carries the file.
Receipts
The user picks up to five photos and sends them. The API checks that every storage key belongs to that user, takes one credit per image, queues a Trigger.dev job and replies straight away. The job has Gemini read each slip and return the supplier, date, total, VAT, tip and a category as structured JSON, and each result is saved as a new row. The phone picks those rows up through Supabase Realtime, so expenses appear already dated and grouped by category.
Mileage and exports
The logbook holds vehicles and trips. Users can type an odometer reading or photograph it and let Gemini read the number. Exports come as an Excel file with one tab per expense category, a SARS-style mileage logbook, and a ZIP holding the Excel file and a PDF booklet of the receipt images. The ZIP is built in a Trigger.dev job too, and another resets plan credits each month.
Key decisions and trade-offs
Read receipts after replying, not while the user waits
- Chose
- Reply at once, run Gemini in a Trigger.dev background job, and push results to the phone through Supabase Realtime
- Over
- Holding the request open until Gemini returns the fields
- Cost
- More moving parts: a job queue, a realtime subscription, and refunds for jobs that fail
On the free Gemini tier a slip could take up to 30 s. That is a long time to stare at a spinner on a phone. Now users send the photo and close the app, and the expense is waiting when they come back. A background job also retries on its own and doesn't die with a serverless function's time limit.
Harden what worked instead of rewriting it
- Chose
- Fix the existing code in place, security holes first
- Over
- A clean rewrite before launch
- Cost
- Living with older code for longer, and a slower cleanup
When I came back to Vatrio in September 2026, an audit showed the core loops worked: receipt reading, expenses, exports and the logbook. The problems were in the trust layer. A user could give themselves a paid plan and credits by writing to their own profile. I closed those holes and released the fixes on 17 September before touching tooling. Capture speed was the same after the refactor.
Let the database be the only authority on money
- Chose
- Atomic Postgres functions that take and refund credits, locked billing columns, and storage keys tied to their owner
- Over
- Checks in each API route
- Cost
- More migrations and database tests, and a production schema that had drifted from the repo had to be reconciled first
Credits pay for AI work, so a double spend or a self-granted credit costs real money. A check in an API route can be missed on one route, or raced by two requests at once. A rule in the database holds on every path. Credits are taken before Gemini is called, all or nothing, and refunded for any image that doesn't become a saved expense. Typing an expense in by hand stays free.
Outcome
I tested Vatrio with 5 users. Each captured the same 10 till slips twice: typed into a spreadsheet by hand, then with Vatrio. The slips covered fuel, a combined groceries and office-supplies slip, entertainment with and without a tip, and a gift receipt.
By hand, a slip took about a minute. With Vatrio it took 12 s on average. Vatrio read 49 of the 50 receipts with every field correct. The one miss was a single date digit, which the user spotted and fixed in about 10 s. The expenses came out ordered by date and grouped with no extra work.
The 5 users also rated the task out of 10. Difficulty went from 7 by hand to 2 with Vatrio, and speed from 2 to 7. From five people that is weaker evidence than the timings. Two things held them back: too many clicks at the time, and waits of up to 30 s on the free Gemini tier. Background processing has since removed the wait. Capture speed was the same before and after the later refactor, so these numbers hold for the live version.
Reflection
The one call I'd reverse is how much I put into trip capture. I built odometer photo reading so a trip could be logged from a picture of the dashboard. It wasn't popular. Users don't mind typing a number, and receipts are where they save time. I'd have spent that effort on the receipt flow instead, starting with the clicks users complained about.
Two things are planned but not shipped. Paystack billing comes next, so plans can be paid for. After that, TensorFlow.js or something similar to crop each photo to the slip on the phone before upload, so Gemini gets a smaller, cleaner image to read.
Automate the chore users hate, not every chore you can.

