WorkThe Cloudflare pivot
Lextract
Read a commercial lease PDF and handed back 126 structured lease terms, each with a confidence score, a citation to the source text, and a red-flag review of the clauses that tend to cost tenants money.
- Ran on
- Railway until June 2026, then Cloudflare Workers
- Ran
- March 2026 to August 2026
- Domain
- Commercial lease abstraction
That platform choice is part of a longer migration: Azure, then Railway, then Cloudflare, across the whole estate. Read how and why.
Retired
Built with
- Next.js 16
- React 19
- TypeScript
- Cloudflare Workers
- Workflows
- Queues
- Neon Postgres
- Stripe
Hosted services
- Railway
- Cloudflare Workers
- Workflows
- Queues
- R2
- Hyperdrive
- D1
- Neon Postgres

The decision
On 2026-06-12, the entire backend moved from FastAPI, Celery, and Redis on Railway to Cloudflare Workers in a single day: 29 commits in the private development history, executed by coding agents against a 1,091-line written plan agreed in advance, with the Celery chain remapped onto Cloudflare Workflow steps and the API contract frozen so the frontend needed no changes.
The Railway backend had worked, but it billed for every hour it sat idle against traffic that was bursty by nature: a lease arrived, minutes of work happened, then nothing for hours.
The Python design it replaced ran a second, adversarial extraction pass that hunted for errors rather than confirmed them, a dual-extract judge that arbitrated genuine disagreements, a per-model pricing table maintained in the repository, and explicit prompt-injection defence around lease text an attacker could author. The Cloudflare orchestrator ran the extraction prompt twice against the same model instead, and the Python stack stayed in the repository, unported, so the two designs could be read side by side.
The new design ran only when a lease came in, matching the traffic pattern that had cost money to sit idle against on Railway.
Architecture
Production ran entirely on Cloudflare, with no container, VM, or always-on process anywhere in the
request path. A Next.js 16 Worker served the app and reverse-proxied /api/v1/* to a second Worker
holding the API.
Extraction and export ran as Cloudflare Workflows, so a transient model error retried only the failing step rather than the whole document. Extraction takes minutes and calls an unreliable third party, which is the whole reason it cannot live inside a request. Neon Postgres sat behind a Hyperdrive binding, since serverless functions and a fixed connection limit do not mix.
The extraction workflow was eight durable steps, with one compensating step catching a throw from any of them:
load PDF, run extraction
persist extraction result
mark scoring
score confidence
persist confidence
detect red flags
persist red flags
mark complete, enqueue email
-> on any throw: mark extraction failed
Each pass inside that first step was a model chain rather than a single model, and the third pass required both conditions at once: a field holding a non-null value scoring below the escalation threshold, and headroom still available under the cost ceiling.
The score itself did not take the model’s word for it. Cross-field validators re-derived what they
could and penalised every field involved when the arithmetic disagreed, and fields genuinely absent
from a lease were scored not_found and excluded from the overall denominator rather than counted
as failures.
The design standards were agreed in writing before any code and were short enough to hold in mind: routes parse and delegate and do nothing else, SQL lives only in repositories, state transitions go through one module, R2 key construction goes through one module, and every external API sits behind an adapter.
The first Playwright end-to-end pass, on 2026-03-24, found the entire authenticated API returning 401 for every registered user: dashboard, profile, credit balance and upload all broken. The cause was the auth SDK’s session cache: it stored the session response in a signed cookie for five minutes, and a cached response carried no headers, so the header carrying the JWT was absent for the whole cache window after every login. The fix bypassed the cache entirely and asked a token endpoint directly.
By the numbers
Each figure carries where it came from and when it was measured. Open one to see the command behind it, where the repository recorded a command.
- Code and tests
- 567 source files / 106,161 lines; 347 test files / 91,084 lines; 4,731 declared test cases
Provenance
Source portfolio/METRICS.md § What each number's command actually is
python scripts/repo-metrics.pyMeasured
- Extraction schema
- 126 fields across 16 categories (44 required, 19 CAM-relevant)
Provenance
Source portfolio/METRICS.md § Extraction schema
Measured
- Frontend tests executed
- 2,282 tests across 190 test files
Provenance
Source README.md § Testing
Measured
- Coverage gates
- Backend and extract-sdk Python: --cov-fail-under=95 with branch coverage; Frontend: lines 88%, functions 80%, branches 80.3%, statements 85.6%
Provenance
Source README.md § Testing
Measured
- Ground-truth extraction harness
- 20 leases with ground-truth cases, drawn from public SEC EDGAR filings
Provenance
Source README.md § Testing
Measured
- The Cloudflare rewrite
- Python/Celery/Railway backend replaced in a single day (29 commits against a 1,091-line written plan)
Provenance
Source README.md § The rewrite: Python to Cloudflare-native, and what it cost
Measured
- Screenshot sweep
- 618 public routes captured, both light and dark theme
Provenance
Source README.md § Screenshots
frontend/scripts/capture-archive.mjsMeasured
- Commit history
- 1,252 commits between 2026-03-03 and 2026-08-07, including 201 merges: 421 fix, 285 chore, 212 feat, 67 docs, 26 test, 16 refactor
Provenance
Source portfolio/METRICS.md § Commit history
Measured
Screens




Its portfolio/ directory holds the architecture, metrics and testing documents these figures were taken from.