Context
We needed to issue export invoices — a handful per year, one client, USD, international wire, with the record-keeping that Indian export documentation requires. The obvious move was a self-hosted open-source invoicing platform, so we deployed one.
It worked. It was also five containers — application, web server, database, cache, and a backup sidecar — with a migration path to maintain, a patch cadence to keep up with, and a feature surface aimed at businesses issuing hundreds of invoices a month with recurring billing, client portals and payment gateways.
For our actual requirement, that ratio was wrong. We decommissioned it the same day and built what we needed instead.
What we built
A single FastAPI container:
- Invoice generation to PDF via WeasyPrint — an HTML/CSS template rendered server-side, so the invoice layout is maintained as CSS rather than as coordinates in a drawing library.
- A SQLite ledger holding every issued invoice, its line items, amounts, dates, and a SHA-256 hash of the generated PDF.
- Sequential numbering in the format
BLR-{Indian financial year}-{0001}, where the number is consumed only on full transaction success — so a failed generation never leaves a gap in the sequence. Gaps in an invoice series are the kind of thing that invites questions during an audit, and they’re trivially avoidable if you handle it at the database transaction boundary rather than in application code. - Due dates and an overdue flag on the ledger view, which is the entire “accounts receivable” feature set a one-client business actually needs.
Authentication: none, deliberately
The application has no login screen, no user table, and no session handling.
It sits behind the same Traefik forward-auth middleware and SSO provider that protects our other internal services, and reads the authenticated identity from a header the proxy sets. Authentication is infrastructure, and it’s already solved there.
This removes the entire category of bugs that comes with hand-rolled auth in a small internal tool — password reset flows, session fixation, remembering to rate-limit the login endpoint. The one thing it demands is that the application must never be reachable except through the proxy, which is a network control, and network controls are the kind of thing we’re comfortable enforcing.
The feature we built and deleted
We implemented cryptographic PDF signing, then removed it entirely once we established that a self-signed certificate carries no legal weight in India and produces a warning banner in Acrobat. The choice wasn’t “signature” versus “no signature” — it was a security warning on every invoice sent to a client versus a clean document.
The PDF hash in the ledger stayed. It’s an internal integrity record, which is a real if modest thing, and unlike the signature it doesn’t claim to be something it isn’t.
Result
One container instead of five. No database server, no cache, no scheduled backup sidecar — the SQLite file is on a volume that the host backup already covers. Deploys are the same pipeline as everything else: push to the repository, CI scans the image, ships it, and restarts the service.
The build took about a day. The platform it replaced would have taken longer than that per year in patching and upgrade work.
Why this is here rather than hidden
It’s an in-house build, not client work, and it’s listed as such. We include it because the decision it illustrates — deploy the standard thing, measure it honestly against the actual requirement, and be willing to throw it away — is the same judgement we bring to client systems. Choosing the smaller thing is usually harder to justify and usually correct.