The starting point
The Tao of Tea's website catalog — roughly 280 products, each with tins, bags, refills, and multi-pack variants — lived in a shared spreadsheet. It mostly worked, but it had the usual spreadsheet problems: anyone could edit anything, prices drifted between copies, and inventory status was whatever the last person typed — including entries like out of stcok, which a spreadsheet accepts without complaint.
Other systems I'd built at Tao depended on this data being right, so it seemed worth putting on firmer ground. This became the sixth system — quieter than the others, but foundational.
Seeding the data
The cleanest existing record of the full catalog was, ironically, the public website. I scraped it — about 34 MB of pages — and parsed products, variants, sizes, and descriptions into structured records, then reconciled that against the spreadsheet's tribal knowledge. Tedious, unglamorous, and the single most valuable step: a database is only as good as its first import.
The decisions
One gatekeeper, no exceptions
A small Rust / axum service is the only thing that talks to the database. Every read and write — from the desktop app, from future tools, from anything — goes through it. No side doors, no direct connections, no "quick manual fix" in a DB console that quietly breaks consistency.
If the service doesn't allow it, it doesn't happen. Most of the system's reliability comes from that one constraint.
Integrity lives in the database, not in discipline
PostgreSQL enforces the rules at the door: valid statuses are an enum, so out of stcok is rejected the moment someone types it, not discovered weeks later. Prices are constrained to sane ranges. Variants must belong to a real product. Relationships are foreign keys, not vibes.
The spreadsheet depended on everyone being careful, always. The database just enforces the rules, which is a more realistic plan.
Cloud, not the office machine
The cheap answer was a PC in the office — I'd built desktop-local systems before, and for those it was the right call. This one isn't: catalog data needs to survive power cuts and hardware changes, and be reachable beyond one machine. Managed Postgres in the cloud, automated backups, access controlled by the service.
A desktop app that stays out of the way
The interface is a Tauri desktop app — one icon, opens fast, search, edit, done. It's used by non-technical staff, so the bar was simple: it has to be easier than the spreadsheet, or people will reasonably keep using the spreadsheet.
Built to outlive me
Same principle as the systems before it: nothing hardcoded that staff might need to change, statuses and categories editable through the app, and documentation written so someone else can run it without calling me. That last part is the real test of whether a system is finished.
Hard problems, honestly
The connection bug that wasn't in my code. Mid-build, the service started failing to reach the database — intermittently, the worst kind. The actual error message pointed at the network layer: the host resolved to IPv6, which my environment couldn't route, while the connection pooler expected otherwise. The fix was routing connections through the pooler's IPv4 endpoint — found by reading the error the machine actually gave rather than the one I assumed it gave. Most of the debugging time went to exactly that.
Modeling identity. The same tea exists as a tin, a pound bag, an eco-refill, and a "Pack of 2" on Amazon — one product, many skins. Getting the product-versus-variant model right mattered more than any line of code, because every other system consumes this answer.
Migration without a pause. The spreadsheet couldn't stop being used while its replacement was born. Old and new ran side by side until the numbers agreed and the habit switched — the same parallel-run pattern as the QuickBooks migration, because it works.
Outcome
The catalog now has a proper source of truth — a service that enforces the rules and a database that rejects bad data at the door. Non-technical staff work with it through the desktop app, and the spreadsheet is on its way to becoming a backup rather than the system of record. Migrations of habit take longer than migrations of data; that's fine. The foundation is in place.
Rust · axum · PostgreSQL · Tauri · Deployed to managed cloud infrastructure