← Work (Case 03 · In testing)

FilmLedger

Expense tracking that survives a film shoot

Role
Solo — data model, receipt scanning, approvals, interface, deployment
Stack
Next.js · Postgres · AI receipt scanning · Cloud storage
Year
2026
(03)

A production-expense system for film crews — receipts photographed in the field, read automatically, sent through an approval chain that matches how a production office actually works, and out the other end as figures a producer can trust the same day. Built as a private deployment for one production house rather than another shared subscription.

The problem

Film money moves faster than paperwork, and nobody on set was hired to do bookkeeping.

A runner buys forty breakfasts at five in the morning. A gaffer replaces a dead cable in a hardware shop with no signal. A location fee gets settled in cash on a roadside. All of it is legitimate, all of it is urgent, and none of these people are going to stop and fill in a form properly.

By the time it reaches the production office it is a shoebox of curling thermal paper and a spreadsheet nobody fully believes. Meanwhile the person running the production needs to know what has been spent today — not next month, when the receipts have faded and the shoot is over.

FilmLedger dashboard: spend to date, receipts awaiting approval, recent productions and costs by category

What I assumed — and what the work taught me

Three things I was confident about. All three were wrong, and one of them was quietly approving money.

I assumed the approval limit protected the money. I built a safety net that checked the amount, felt rather good about it, and never once asked whether the amount was correct. It wasn’t. A receipt the scanner had barely a third understood came in under the limit and approved itself before a human ever saw it. I found out by accident — a test expected that receipt to be sitting in the approval queue and it simply wasn’t there. An amount threshold asks how much is at stake. It never asks whether the number is real. Those turn out to be different questions.

FilmLedger receipts list grouped by month, each row showing category, production and approval status

I assumed the slow pages meant slow code. The code was fine. The geography wasn’t. The application was running in Washington while its database sat in Singapore, so every single request crossed the Pacific and came back — and a page that displayed no data at all still took a third of a second to say so. On top of that sat a dozen queries running one after another instead of together, the same login checked twice on every page, and receipt lists shipping whole photographs as text: up to 36 MB to draw one page. I could have spent a week tidying the code and improved nothing.

I assumed an invitation was a simple thing. Three separate parts of the app asked “is this invite still valid?” and not one of them checked whether it had expired — including a setup checklist that cheerfully ticked itself off against invitations nobody could use. The invite link also only existed inside the dialog that created it, so closing the window lost it, and re-sending meant cancelling the invitation you had already sent. Both were invisible in the code and obvious within seconds of a real person trying to invite somebody.

The limit asked how much was at stake. Never whether the number was real.

How I solved it

Photograph a receipt in the field. It gets read, filed, approved by the right person, and counted — the same day.

The software reads the receipt — vendor, date, total, category — and says how confident it is about what it read. It lands in the right production, follows an approval path that mirrors the real office (crew submit, coordinators approve, producers see everything), and comes out as figures and exports that add up.

The small decisions matter more than the big ones here. Receipts upload in batches, because nobody photographs one at a time. Vendor names are matched across the half-dozen spellings a crew will type for the same shop. Dates are stored so they can’t drift a day when a server changes timezone — a bug I found and fixed before it could quietly misfile a week of spending.

The whole thing is one deployment per production house: their own instance, their own database, their own storage. It is covered by about 130 automated tests, because the failure that matters here — money quietly going to the wrong place — is exactly the kind that never announces itself.

130automated tests
faster page response
36 MBcut from one page load
4rounds of feedback shipped
1deployment per client
0data shared with other companies

Where it is

Live, in testing with a production house in Kuala Lumpur, on their own instance.

It is being put through real productions now, on servers in the same region they work in. Every round of their feedback has come back as shipped work — and every bug above was found that way, by people using the thing, rather than by me reading my own code and admiring it.