Skip to content

06 / Personal project · 2026

Stop retyping invoices. Start clearing cargo.

Supplier invoice PDFs become Dubai Customs filings — extraction, HS-coded line items, and a reviewable Bill of Entry, built solo in two weeks.

Role
Solo Developer
Timeline
2 weeks
Type
Customs documentation SaaS
Stack
Laravel 13 · Vue 3 · Inertia 3 · TypeScript · Tailwind CSS 4 · Python · PyMuPDF · Stripe · Pest

At a glance

  • Freight forwarders retype supplier invoices into Dubai Customs declarations by hand. EMSDOC turns the PDF into a reviewable Bill of Entry draft instead.
  • Hybrid PHP → Python pipeline: PyMuPDF runs in a persistent Unix-socket daemon, cutting 200–400ms per request and holding memory flat at ~50–100MB on a tiny VPS.
  • Dual LLM extraction engines at temperature 0, with token-aware chunking, result merging, and a four-provider OCR fallback chain capped per document for cost control.
  • Full SaaS shape in two weeks: Stripe-metered plans in AED, 2FA + WebAuthn passkeys, admin panel, and an audit log of every extraction step. 207 tests, 62 commits, all solo.

01 / Context

The declaration nobody wants to type

Clearing cargo through Jebel Ali or DXB means filing a Bill of Entry with Dubai Customs — and the source data arrives as supplier invoices: PDFs in every layout, language, and currency, sometimes 500+ line items with mixed HS codes. Operators retype them field by field.

EMSDOC's pitch: drop the PDF in, get a declaration draft out. The platform extracts invoice number, supplier, buyer, and HS-coded line items, shows source document and extracted fields side by side for review, then exports the formats the government actually accepts — Dubai Customs' fixed-width text rows and the MOHAP medical-device import CSV.

02 / Constraints

Small hardware, government deadlines

  • Deployment target: the smallest VPS available. One to four vCPUs, a few GB of RAM, ~20 concurrent users. No GPU box for OCR, no managed PDF service budget.
  • Extraction must be boring. A customs filing is a legal document. The LLM layer runs at temperature 0 and every step is logged with payloads and durations, so any number can be traced back to a page.
  • Cost ceilings. OCR fallback across four providers means per-page pricing matters; scanned pages are capped per document (default 20) so a bad upload can't burn a plan's margin.

03 / Pipeline

PHP for the product, Python for the paper

Laravel owns the product — auth, billing, review UI, exports. But the serious PDF tooling lives in Python, so extraction runs as a PyMuPDF sidecar with two pluggable transports: a plain CLI subprocess, or a persistent Unix-socket daemon that keeps the library loaded between requests.

The daemon is what makes a tiny VPS viable: process spawn per request costs 200–400ms and a fresh memory footprint each time; the daemon holds PyMuPDF resident at a flat ~50–100MB and answers over the socket. Extraction jobs run on a dedicated pdf queue, so a heavy upload never blocks a billing webhook.

04 / Extraction

LLMs, kept on a short leash

Two extraction engines sit behind one orchestrator: the laravel/ai SDK agent and a raw HTTP engine, both pinned to temperature 0. Long invoices exceed context windows, so the pipeline chunks text with a 200-token overlap against per-model window sizes stored in the database, then merges partial results deterministically.

When a page has no text layer, the OCR fallback chain walks four providers from cheapest up. Every attempt — engine, chunk, provider, duration, error — lands in ExtractionAuditLog, and extraction results are encrypted at rest. The audit table is the feature: a forwarder disputing a filing needs receipts, not vibes.

05 / The SaaS around it

Billing, auth, and admin in the same two weeks

Cashier runs metered Stripe billing: plans priced in AED with monthly page quotas, trial units, usage records, and Stripe meter events. Auth is Fortify with 2FA and recovery codes, WebAuthn passkeys, and Sanctum for tokens. An admin panel, role middleware, and subscription gates round it out.

None of this is exotic, which is the point — the product is the pipeline, and the SaaS chassis just has to be correct. 207 Pest tests keep it that way.

Pest tests
weeks, first commit to launch
line items per invoice
commits, all solo

06 / Outcome

Two weeks of work, completing next week

62 commits between the first scaffold and a working SaaS: upload, extraction, side-by-side review, customs-format export, plans, payment, admin. The landing page is live with AED pricing, and the product completes next week — my most recent work, built to be validated with the forwarders it was made for.

The engineering bet — boring extraction with full audit receipts — is the part I'd defend today. The market test starts at launch.

07 / Lessons

What two weeks of customs taught me

  • Sidecar daemons beat microservices on small hardware. One Unix socket removed a whole deployment tier.
  • Regulated output means determinism is a feature: temperature 0, logged everything, merged results you can re-derive.
  • Chunking is a data problem before it's a prompt problem. Per-model context windows belong in the database, not in a config comment.
  • Speed comes from constraints. Small hardware and a two-week window cut more scope than any planning session would have.