Technical demo · Amr Hazem

AI Document Extraction WorkBench

Built to carry 20 million documents when scaled horizontally.

A queue-leveled extraction pipeline: presigned direct-to-S3 uploads, stateless worker fleet, one writer to Postgres, and real-time push back to the clinic. Throughput is a function of worker count, not of application code.

20MDocumentsSupported capacity with horizontal scalling.
0 droppedFault resilienceExponential backoff retries and SQS Dead-Letter Queues guarantee zero lost referrals.
< 50msReal-time dispatchPostgres LISTEN/NOTIFY streams extraction events straight to the client with zero polling.
20 MBMax per PDFUploaded direct to S3 — never through the app server.
15+Lanes per workerParallel polling lanes per replica — concurrency before you even add instances.
3 daysExecution timeDesign, build and demo delivered end to end in three days.

Workflow lifecycle

1Authenticate

Stateless JWT carries clinicId; every repository call and cache lookup is scoped to that tenant.

JWT guard
2Request upload slot

API creates PENDING rows, resolves the extraction schema and issues presigned S3 PUT URLs.

POST /referrals
3Direct-to-S3 upload

The browser pushes the PDF straight to object storage, keeping app bandwidth flat under load.

Presigned PUT
4Queue notification

S3 emits ObjectCreated into the upload queue — the buffer that absorbs traffic spikes.

SQS
5Worker claim

A stateless worker long-polls and claims the job with an atomic Redis lock; duplicates skip safely.

SET NX EX
6Validate & extract

Magic-byte and size checks, then multimodal extraction into an enforced JSON schema with backoff retries.

Gemini Flash
7Publish result

Worker publishes COMPLETED / REJECTED / FAILED to the status queue; the API applies it as the single writer.

Status queue
8Push to the clinic

A Postgres trigger notifies, and the row streams to the open dashboard connection in real time.

LISTEN → SSE
Concurrency model — parallel worker lanes

The Agent Worker runs a configurable pool of parallel lanes (default 15), each independently long-polling SQS one message at a time, so many referrals extract concurrently and one slow LLM call never blocks the rest. Lanes are fault-isolated and coordinate safely through the Redis claim lock — scaling out is just running more worker replicas.

Deployment

Everything above — auth, uploads, the queue, the worker pool, Postgres, Redis, real-time push — runs on one AWS EC2 t3.micro instance. 1 vCPU, 1 GB RAM, under $5/month.

This isn't the target production topology — it's a stress test of the architecture itself. Presigned S3 uploads, a queue-buffered worker pool, and a single Postgres writer were designed to scale horizontally across a fleet of machines; running the entire pipeline shoulder-to-shoulder on one micro instance proves the design holds under real constraints, not just on a whiteboard. Getting from here to production scale is additive — more worker replicas, a managed database, a load balancer — never a rewrite.

Single instance, six containers

Caddy, web, API, worker, Postgres, and Redis, all co-located on one t3.micro, coordinated over Docker Compose's internal network.

Zero-touch HTTPS

Caddy obtains and renews a real Let's Encrypt certificate automatically — no manual cert management.

Under $5/month

Free-tier EC2 hours plus one public IPv4. No managed database, no load balancer, no NAT gateway.

Scale is additive, not a rewrite

The same horizontal-scaling levers from the section above are what let this run lean today and wide tomorrow.

What makes it scale

Parallel worker lanes a configurable pool (default 15) long-polls SQS one message per lane, so a slow LLM call never blocks the rest; lanes are fault-isolated and coordinate via the Redis claim lock.

Queue-based load leveling spikes land in SQS, not in the API; consumers drain at their own rate.

Stateless workers no DB connections to exhaust, so capacity grows linearly by running more replicas.

Bandwidth off the app path presigned S3 uploads keep payload bytes out of the application request path.

Cache-aside + O(1) index Redis serves schema and list reads; Postgres handles writes only.

Single-writer aggregates idempotent DDD transitions make at-least-once delivery safe.

Indexed tenant reads clinicId + createdAt index keeps history paging constant-time at volume.

Stack

Frontend
Next.js 16TypeScriptTailwind v4react-pdfZustand
API
NestJSDDD / HexagonalPrismaOpenAPI
Worker
Node.js daemonSQS consumerRedis locks
Data & infra
PostgreSQLRedisAWS S3SQSDocker Compose
AI
Gemini FlashStructured JSON schemaBounding boxes
Business scope, briefly.

Per-clinic history that reopens into full detail, and JWT tenant isolation across REST, cache and SSE — table stakes, not the interesting part.