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.
Workflow lifecycle
Stateless JWT carries clinicId; every repository call and cache lookup is scoped to that tenant.
JWT guardAPI creates PENDING rows, resolves the extraction schema and issues presigned S3 PUT URLs.
POST /referralsThe browser pushes the PDF straight to object storage, keeping app bandwidth flat under load.
Presigned PUTS3 emits ObjectCreated into the upload queue — the buffer that absorbs traffic spikes.
SQSA stateless worker long-polls and claims the job with an atomic Redis lock; duplicates skip safely.
SET NX EXMagic-byte and size checks, then multimodal extraction into an enforced JSON schema with backoff retries.
Gemini FlashWorker publishes COMPLETED / REJECTED / FAILED to the status queue; the API applies it as the single writer.
Status queueA Postgres trigger notifies, and the row streams to the open dashboard connection in real time.
LISTEN → SSEThe 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.
Caddy, web, API, worker, Postgres, and Redis, all co-located on one t3.micro, coordinated over Docker Compose's internal network.
Caddy obtains and renews a real Let's Encrypt certificate automatically — no manual cert management.
Free-tier EC2 hours plus one public IPv4. No managed database, no load balancer, no NAT gateway.
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
Per-clinic history that reopens into full detail, and JWT tenant isolation across REST, cache and SSE — table stakes, not the interesting part.
