BeforeMemory, chalk and paper slips. No record. 
AfterEvery garment tagged, timed and traced.
- Steps, intake to receipt
- 7
- API endpoints
- 65
- Desktop builds
- 2
- Auto-sent messages
- 0
Intake to receipt
FixQR tags printed per household, one per garment. FixScan-in starts a return clock. Overdue raises an alert. ResultThe receipt keeps the shop's own slip layout. gateThe system drafts the message. A person sends it. shipOne installer. No Docker, no IT.
On the counter

printTag sheets laid out for the shop's printer. 
trackOne row per garment, with its time left. 
catalogueCloth types set the water saved per wash.
Read the engineering detail
The problem
A laundry handling dozens of households a day tracks garments by memory, chalk marks and paper slips. Items get separated from their batch. When a customer says a shirt is missing, there is no record on either side. Two people have different recollections, and the shop pays for the shirt to keep the customer.
What I built
Every item gets a printed QR tag at intake, bound to a household. Tags are read by a camera or a USB barcode scanner. The counter UI creates or looks up a household, scans its garments into a batch, and seals the batch when the bag leaves the counter. Each garment then carries its own return window. Anything past it surfaces as a missing-item alert instead of being discovered through a complaint. Scanning a garment out on return closes it, completes the batch when the last one lands, and counts the water saved for its cloth type.
The receipt matches the format the shop already used. The customer gets a WhatsApp message that the system composes and a person sends.
It runs as a desktop app on the counter of a laundry in Africa, which is where it is used today.
The constraint that shaped it
The shop counter is one PC, run by staff who are not going to install Python, and the internet connection is not guaranteed.
That drove the architecture. The application is a FastAPI service with
server-rendered pages, so there is no build step and no node_modules on a shop
counter. PyInstaller packages it into a Windows .exe and a macOS .app with
a launcher, so it starts from an icon on the desktop and opens in the browser.
The desktop build runs on a single SQLite file in the operating system’s
user-data folder, so the install stays read-only and the data stays writable.
The same code runs against Supabase Postgres when hosted.
Sending receipts in bulk
A single send is one click per household, so staff can also select several finished batches and send them together. The messages go through OpenWA, a self-hosted WhatsApp gateway, with the receipt rendered to an image by headless Chromium. Sends are throttled with a few seconds and some random jitter between them, one failure never stops the rest, and a batch that has already been sent is skipped unless staff ask to resend. It is still a person who picks the batches and presses send.
OpenWA drives WhatsApp through an unofficial client, not Meta’s paid API, so the sending number can be restricted. The spec accepts that risk explicitly: a dedicated number, throttled sends, and only real receipts to customers who expect them.
The gateway, its Node runtime and the Chromium build all ship inside the installer. The shop installs one thing, with no Docker and nothing to download.
What I learned
This is the least technically ambitious project here and the one I learned the most from, because the hard constraints were all non-technical: what the staff will tolerate, what breaks when the shop is busy, and how a system fails when the person using it is in a hurry.
Media


How this was built
The spec sheet above lists the stack. This section covers how the work was run, which a screenshot cannot show you.
Where AI did the work
- Generated the schema, ORM models and validation layer as one coherent pass so the database constraints and the Pydantic schemas enforce the same rules instead of drifting apart.
- Kept a running progress log across sessions, so each one resumed from the recorded state of the build instead of a guess at it.
The discipline around it
- Architecture and setup are separate documents from the README: one for someone extending the code, one for someone trying to get it running on the shop counter.
- Receipt rendering was built against photographs of the receipts the shop already used, rather than a layout I invented.
- Larger features started as a written spec with their decisions and known risks, agreed before any code was written.
What stayed human
- Nothing auto-sends to a customer. The WhatsApp message is composed by the system and released by the person at the counter, because a wrong automated message to a customer is worse than no message.
- Barcode uniqueness is enforced at the database level rather than in application logic, since a duplicate tag would undo everything the system is for.
Process artifacts in the repo
- CLAUDE.md
- The working agreement for AI sessions, covering the version history, discuss-before-build and manual migrations.
- tasks_wip/
- One spec per feature, with decisions, risks and phases, written before the code.
- release_steps.md
- The runbook for building and handing over the Windows .exe and macOS .app.