The brief
A friend-of-a-friend ran a small design studio out of a converted apartment in Lalitpur. She had three employees, twelve repeat clients, and a spreadsheet that crashed Excel once a month. She wanted something that "just shows me how much money I have, and what I'm owed." That was the brief. I built it.
The shape
Django, because I knew I was going to be the only person maintaining it and I wanted the batteries-included path. PostgreSQL because money. HTMX on the front-end because I refused to spin up a separate React app for what amounts to four CRUD screens and a dashboard. Tailwind for styling. Caddy in front, deployed on a single droplet for $6 a month.
What real-world data taught me
I'd built CRUD apps before. I'd never built one that handled actual money for actual people. Three things came as a surprise.
1. Money cannot be a float.
Obvious in hindsight. Less obvious when you're 200 lines into a model
and Django's DecimalField is annoying you. I learned
this from the bug report I least wanted to receive: "the total at
the bottom doesn't match if I sum the line items myself."
2. Timezones are not optional.
The client did business in three different currencies and travelled
a lot. An invoice issued "today" needed to mean today in Kathmandu
even when she was in Berlin. I learned more about
USE_TZ=True than I ever wanted to.
3. Real users don't read modals.
I added a confirmation modal before deleting an invoice. The first accidental deletion happened on day four. I added a soft-delete system that night. Two years in, the modal has prevented zero accidents; the soft-delete has saved her about a dozen times.
Design the system for what users will actually do, not for what you wish they'd do. The right answer was almost never the warning dialog — it was the undo button.
What it looks like now
- ~3,200 lines of Python (models, views, services).
- ~900 lines of templates.
- Two years of daily use, never a serious outage.
- Total bills sent: 487. Total bug reports I had to actually fix: 9.
What I'd do differently
- I'd start with audit logging from day one. I added it in month four after the soft-delete incident and had to backfill from cold backups.
- I'd factor "money" into its own type, not lean on
Decimalalone. Currency conversion errors are subtle and I'd rather make them impossible to express. - I'd write the month-end dashboard last, not first. I built it first because it was fun, and ended up rewriting it twice as the data model evolved.