The problem
A supplier sends an invoice as a PDF. Somebody opens it next to a price list and checks it line by line — is that the right SKU, is that the contracted price, is that the right unit of measure. It's slow, it's error-prone, and the knowledge gained ("this vendor calls SKU-4471 a Widget Pro 2K") evaporates the moment the check is done. Next month, the same person does the same work on the same vendor's invoice.
What we built
Rectify was my team's project for Cloudflare Singapore Developers Day, built for the hackathon on 27 August 2026, and it's live on Workers.
Upload an invoice. It's parsed to structured JSON, every line is matched against a canonical product standard, and every field difference is flagged. A reviewer accepts, rejects, or edits each flag in a live UI. Accepting a correction writes back into the standard — the corrected price, and the vendor's odd product name recorded as an alias. Then a clean, corrected invoice is published as a PDF.
The payoff: the standard gets smarter with every document. Invoice #1 needs manual decisions. Invoice #2 from the same vendor auto-matches, because the system learned.
The boundary we held throughout: the model reads and compares; it does not decide. Only a human resolution changes the standard.
How it works
PDF upload
→ extraction to structured JSON (Workers AI + Browser Rendering)
→ line-by-line match vs. the standard (exact, alias, then vector search)
→ flags reviewed by a human (accept / reject / edit, live UI)
→ accepted corrections update the standard (price + vendor alias)
→ corrected invoice published as PDF
Fuzzy product matching uses Vectorize, so a vendor's "Widget Pro 2K" can still find SKU-4471 before any alias exists — the human then confirms it once, and it's an exact alias match forever after.
Built for a demo that cannot fail
Hackathon demos die on stage, so the project has a drop ladder:
wrangler.local.jsoncruns the entire session flow — upload, review, publish — in Miniflare with no Cloudflare login?demo=1answers every API call from fixtures, so the UI works with no backend at all- Two Playwright E2E scripts walk the full flow: one against the fixture demo, one against the real backend
- The 30-second pitch trailer is rendered with Remotion from the same fixture data, so every figure on screen is real — invoice NW-INV-24817, S$4,325.34 corrected to S$4,255.58, overstated by S$69.76 across four lines
Tech stack
| Layer | Technology | Why |
|---|---|---|
| Compute | Cloudflare Workers | The whole app, one deploy |
| Extraction | Workers AI + Browser Rendering | PDF to structured JSON |
| Fuzzy matching | Vectorize | Embedding search over the product standard |
| Contract | Shared TypeScript contracts | One frozen file every workstream codes against |
| Local dev | Miniflare via Wrangler 4 | Full flow with no Cloudflare account |
| Trailer | Remotion | The pitch video is code, rendered from fixtures |
What building this taught me
1. A frozen contract is how a team parallelizes in a day
We split the work one file per person, all coding against contracts.ts, frozen early. Nobody blocked on anybody: the UI ran on fixtures while extraction was still being built, and everything met in the middle because the seam was decided first.
2. Human-in-the-loop is an architecture, not a checkbox
Deciding that only a human resolution mutates the standard simplified everything downstream: the model's output is always a proposal, the review UI is the only write path, and the audit trail falls out for free.
3. Degradation ladders beat rehearsal
The fixture-backed demo mode wasn't a fallback we hoped to avoid — it made development faster all week, made the E2E tests deterministic, and meant the live pitch had a floor it could not fall through.
