~/kevin-hoang
2026Live

Punch List

Creator & Full-Stack Engineer

Punch List is an offline-first progressive web app that runs a one-person trade business from a phone: jobs, estimates, invoices, receipts, and mileage. Every screen reads and writes a database on the device, so the app keeps working with zero signal, and finished documents are rendered as PDFs on the phone itself. I designed and built it end to end.

React
TypeScript
Vite
IndexedDB
Dexie
Supabase
pdf-lib
PDF.js
PWA
Visit the live site

How does Punch List keep working with no signal?

The device is the working copy. All data lives in IndexedDB through Dexie, photos are stored as Blobs, and no screen ever waits on the network. A Workbox service worker (vite-plugin-pwa) precaches the full app, including the font files the PDF renderer needs, so it opens instantly from the home screen and can produce a finished invoice in a basement with no reception.

An offline Playwright test runs the same job, receipt, estimate, invoice, and payment walkthrough as the main end-to-end test, with the network switched off. Working offline is something the tests prove, not something the README claims.

How does the two-way sync with Supabase work?

Sync is layered on top of the local database, not underneath it. A sync engine moves changes in both directions between Dexie and Postgres whenever a connection exists, keeping a watermark per table so each pull fetches only the rows changed since the last one.

Pushes run in dependency order (settings, clients, properties, jobs, photos, then documents and payments), so another device never receives a job whose client has not arrived yet. Photo Blobs upload to storage separately, after the rows that point at them.

Why render invoices on the phone instead of a server?

There is no document server to go down or pay for. pdfmake builds every invoice, estimate, and receipt in the browser with the Inter typeface embedded, and a single layout module defines the whole print design (type scale, rules, totals block, and balance band), so all three document types change together.

To send a document as an image that previews inline in a text message, pdf.js rasterizes the generated PDF to a PNG, so the image is the PDF rather than a separately drawn lookalike. Supplier receipts attached to an invoice are merged in with pdf-lib, each on a page of its own, scaled to fit and turned the right way up. Sharing uses the Web Share API, falling back to a download on desktop.

What keeps the financial records correct?

Money is stored as integer cents everywhere, and dates are ISO strings parsed as local time, never UTC, so an invoice is never dated a day early. Document numbers are allocated inside a read-write IndexedDB transaction, so a double-tapped button cannot mint two invoices with the same number, and the sequences have no gaps. Invoice and estimate numbers carry the year and reset each January.

Issued documents snapshot the business and client details at the moment they are issued, so editing a client's address next year does not rewrite last year's invoice. Every generated file is kept against its invoice or estimate as a record of exactly what the customer was given.

How are backups and the print design tested?

A backup is a zip built with fflate: a JSON manifest, the original photos, and every kept document, plus CSVs of income, expenses, and mileage for an accountant. A preview script renders every document variation (full, labor-only, paid, multi-page, estimate, and both receipt states) so the print design can be reviewed without a browser. Development runs in Docker, with an optional local Supabase stack so nothing touches production data.

Want to talk through how it was built?

I'm open to remote and Massachusetts roles. Happy to walk through the architecture, the trade-offs, or the code.