All projects
Python web Client work

shelf

A bookkeeping app I built for one freelance client and ended up maintaining for two years. Invoices, expenses, recurring billings, and a month-end dashboard that fits on one screen.

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.

lesson

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