PlatformUSAII 2026Document IntelligenceJune 2026

ClearPath — AI Document Intelligence Platform

Full-Stack Engineer & AI Pipeline Architect

🚧

Currently Working

We're improving this project and it will be online soon.

Live Demo

This project is still under development and may contain bugs.

ClearPath — AI Document Intelligence Platform preview

TL;DR

Architected and built ClearPath, an AI document intelligence platform designed to decode complex official documents. Engineered a highly reliable multi-process backend (Node API + Dispatcher + Workers + Python/Docling OCR) featuring a transactional outbox, three isolated BullMQ queues (hot path, background path, Python OCR), 8 atomic preprocessing stages with chunking + embedding moved off the user-visible hot path for a 30–35% speedup and reduced memory footprint, real-time lossless SSE updates, and a 5-stage LLM pipeline grounded in official .gov/.edu sources to prevent hallucinations.

Architecture4-process backend (API, Outbox Dispatcher, BullMQ Workers, Python/Docling OCR)
Pipeline5-stage hot path (init → OCR → cleaning → structuring → completion) + parallel chunking/embedding background path + 5-stage LLM pipeline + Human-in-the-loop verification gate
QueuesThree isolated BullMQ queues (Node ANALYSIS_QUEUE_NAME hot path, Node BACKGROUND_QUEUE_NAME for chunking/embedding, Python OCR_QUEUE_NAME)
Performance30 to 35% faster time-to-result on the user-visible pipeline by moving chunking + embedding off the hot path (same output quality)
MemorySignificantly reduced hot-path memory footprint — the embedding model and chunker live in a separate background worker and no longer compete for memory with the result-producing stages
RealtimeLossless SSE streaming via Postgres event replay + Redis Pub/Sub
🖼️

System Architecture Pipeline

🌐
Frontend (Next.js)client Component
➡️ Target Link:API Server (Express)

↳ Upload / SSE Stream / Confirm

🚀
API Server (Express)server Component
➡️ Target Link:PostgreSQL (Supabase)

↳ Save State & Outbox (TXN)

➡️ Target Link:Frontend (Next.js)

↳ Push Real-time UI

⚙️
Outbox Dispatcherservice Component
➡️ Target Link:Redis (BullMQ + Pub/Sub)

↳ Enqueue Jobs

👷
Hot-Path Worker (Node) — init → OCR → cleaning → structuring → completionworker Component
➡️ Target Link:PostgreSQL (Supabase)

↳ AWAITING_VERIFICATION (hot path result)

➡️ Target Link:Redis (BullMQ + Pub/Sub)

↳ Publish Events

👷
Background Worker (Node) — chunking + embedding (parallel, non-blocking)worker Component
➡️ Target Link:PostgreSQL (Supabase)

↳ Persist Chunks + Embeddings (background)

➡️ Target Link:Redis (BullMQ + Pub/Sub)

↳ Publish Events

👷
AI Analysis Worker (Node)worker Component
➡️ Target Link:Groq API (Llama 3.3 70B)

↳ 5-Stage Inference

➡️ Target Link:Tavily Search API (.gov/.edu)

↳ Source Grounding

➡️ Target Link:PostgreSQL (Supabase)

↳ Persist Results

➡️ Target Link:Redis (BullMQ + Pub/Sub)

↳ Publish Events

👷
Docling OCR Service (Python)worker Component
➡️ Target Link:PostgreSQL (Supabase)

↳ Write Outbox (Supabase)

🗄️
PostgreSQL (Supabase)database Component
➡️ Target Link:Outbox Dispatcher

↳ LISTEN / NOTIFY + Polling

⚡
Redis (BullMQ + Pub/Sub)cache Component
➡️ Target Link:Docling OCR Service (Python)

↳ OCR_QUEUE_NAME

➡️ Target Link:Hot-Path Worker (Node) — init → OCR → cleaning → structuring → completion

↳ ANALYSIS_QUEUE_NAME (hot path)

➡️ Target Link:Background Worker (Node) — chunking + embedding (parallel, non-blocking)

↳ BACKGROUND_QUEUE_NAME (chunking + embedding)

➡️ Target Link:AI Analysis Worker (Node)

↳ ANALYSIS_QUEUE_NAME (5-stage AI)

➡️ Target Link:API Server (Express)

↳ Subscribe Events

☁️
Groq API (Llama 3.3 70B)external Component
☁️
Tavily Search API (.gov/.edu)external Component
🛠️

Tech Stack

Next.jsExpressTypeScriptPythonDocling OCRPostgreSQLSupabaseBullMQRedisGroq (Llama 3.3 70B)Tavily APISSE
🎯

Problem

High-stakes documents like immigration notices or benefit letters are notoriously difficult to understand, especially for non-native speakers. A simple misunderstanding can lead to missed deadlines or legal peril. Standard LLM wrappers are too risky for this because they hallucinate facts and invent URLs. On top of accuracy, a multi-minute analysis pipeline must survive process crashes and network drops without losing state — and the user must be able to verify the extracted text before any LLM tokens are spent on it. Performance is also a feature: every stage that runs inline on the hot path is wall-clock time the user is waiting, and every model loaded in the hot-path worker is memory the result-producing stages have to fight for.

⚠️

Constraints

Zero tolerance for lost processing jobs if any process crashes mid-pipeline. Strict requirement for LLM accuracy/grounding — every trusted URL must come from a real Tavily search result. Protection against malicious prompt injections inside user-uploaded PDFs. Long-running multi-minute analyses that must stream real-time progress to the client and recover gracefully on reconnect. OCR is a Python service (Docling/RapidOCR); the Node worker and Python service must share one outbox without claiming each other's jobs. Hot-path performance and memory: chunking + embedding are necessary for retrieval quality but they were the slowest stages on the user-visible path, and loading the embedding model was a meaningful share of worker memory.

👤

My Role

Full-Stack Engineer & AI Pipeline Architect — designed and implemented the monorepo, the 12-file database migration set, custom RS256 JWT auth, the Outbox Dispatcher, the three BullMQ queues (hot path, background, Python OCR), the Node worker orchestration, the Python/Docling OCR service, the SSE streaming layer, and the 5-stage AI pipeline.

🏗️

Architecture

ClearPath is an event-driven, decoupled, multi-runtime system with two parallel Node paths and one Python path. The Next.js frontend talks to the Express API for uploads, auth, confirm-extraction, history, and SSE. The API writes all state changes to Postgres in transactions that also INSERT into document_pipeline_outbox. The Outbox Dispatcher (separate process) subscribes to Postgres NOTIFY on outbox inserts and polls as a safety net, then enqueues the next stage's BullMQ job. Three queues exist: ANALYSIS_QUEUE_NAME (Node hot path: initialization → OCR → cleaning → structuring → completion), BACKGROUND_QUEUE_NAME (Node background path: chunking → embedding — runs in parallel, never blocks the user), and OCR_QUEUE_NAME (Python Docling). The Python OCR service writes its document.extracted outbox event via the Supabase client, so the same NOTIFY triggers the next Node stage regardless of which runtime wrote the row. Each worker stage persists its result and publishes progress to Redis Pub/Sub for live SSE clients while also writing a row to document_pipeline_events so reconnecting clients can replay from their Last-Event-ID. The API's SSE endpoint keeps one Redis subscriber per active client, replays any missed events from Postgres, and emits periodic heartbeats. Stage transitions are guarded by isStageCompleteOrPast() so BullMQ retries are idempotent. Idempotency on analyze requests is enforced both at the DB level (unique index on idempotency_key) and at the queue level (BullMQ job ID = analysis:<requestId>). The hot-path result ships the moment the completion stage fires, while the background path keeps running until chunks and embeddings land in Postgres for retrieval — net 30–35% faster time-to-result with the same output quality.

🗺️

Approach

  • Prioritize reliability: Use the outbox pattern so the database transaction and queue dispatch are fundamentally tied together — if the DB write commits, the job is guaranteed to be enqueued eventually, and if it doesn't commit, no job is enqueued
  • Atomicity over batching: every pipeline stage is a standalone BullMQ job that loads what it needs, does its work, writes an outbox event, and exits. This means any stage can crash, restart, or scale horizontally without losing pipeline state — state lives in Postgres, not in worker memory
  • Separate the hot path from the background path: not every stage is on the user's critical path. Chunking + embedding are necessary for retrieval but they were the slowest stages on the user-visible path — moving them to a parallel BullMQ queue (BACKGROUND_QUEUE_NAME) returned the user-visible result 30–35% faster and let the embedding model live in a separate worker that doesn't compete for memory with the result-producing stages
  • Isolate heterogeneous runtimes: the Python Docling OCR service runs in its own process on a dedicated BullMQ queue (OCR_QUEUE_NAME) so the Node worker and the Python service can never claim each other's jobs
  • Human-in-the-loop at the extraction gate: stop the pipeline after raw text extraction so the user can verify dates, contacts, and text before any LLM tokens are spent on analysis — supported by PATCH auto-save and POST confirm-extraction endpoints
  • Defensive LLM engineering: every pipeline stage has strict Zod schema validation and a safe fallback. The pipeline gracefully degrades (sets needs_human_review: true) rather than crashing, and every trusted URL in the final output is a subset of the URLs Tavily actually returned
  • Lossless real-time UX: combine Redis Pub/Sub for live updates with Postgres event-log replay via Last-Event-ID. A 2-second network blip must not drop a single pipeline event — and the client must auto-recover via an active session-restore on page load
✅

Responsibilities

  • Designed the full database schema in Supabase PostgreSQL — 12 SQL migrations covering users, documents, analysis requests, pipeline events, sections, chunks, facts, results, the outbox, RLS grants, and the AWAITING_VERIFICATION / AI_* enum extensions
  • Implemented the Outbox Dispatcher (src/outbox/dispatcher.ts) using PostgreSQL LISTEN/NOTIFY alongside a polling fallback, with FOR UPDATE SKIP LOCKED on the outbox so multiple dispatcher instances never double-process
  • Refactored the pipeline into two parallel paths: a 5-stage hot path (stage-initialization → extract-layout-and-ocr → cleaning → structuring → completion) that the user is waiting on, and a background path (chunking + embedding) running on its own BullMQ queue — chunking and embedding are persisted to Postgres asynchronously and used for retrieval, while the user-visible result ships the moment the hot path completes, achieving a 30–35% speedup with the same output quality and a substantially reduced hot-path memory footprint
  • Split preprocessing into 8 atomic stages in total across both paths — every stage is its own BullMQ job, every transition writes a document_pipeline_outbox row in the same transaction as the state update, and isStageCompleteOrPast() guards keep retries idempotent across paths
  • Built the isolated Python/Docling OCR service (app/backend/services/ocr-engine/) — runs in its own process, consumes from a dedicated OCR_QUEUE_NAME, writes the document.extracted outbox event via Supabase, and never connects to the Node worker's queue
  • Constructed the 5-stage AI worker pipeline: Document Understanding, Candidate Extraction, Grounding & Verification (via Tavily restricted to .gov/.edu), User-Facing Synthesis, and Safety Review with both LLM-judged and rule-based guardrails
  • Engineered lossless SSE infrastructure: per-client Redis subscriber connections, Postgres event-log replay from Last-Event-ID on reconnect, periodic heartbeats, and an active /analysis/running-check session restore on page load so users can navigate away and back without losing their in-flight analysis
  • Implemented defensive LLM engineering — every stage validates with Zod, returns a safe fallback on failure (sets needs_human_review: true), enforces uncertainty language in the summary, normalises questions, and sanitises trusted_sources to URLs that actually appeared in the Tavily results
  • Secured auth end-to-end: RS256 JWT access + refresh tokens in httpOnly cookies, sid cookie for session tracking, argon2 password hashing, express-rate-limit on login and refresh, x-internal-api-key guard on the dispatcher endpoint
  • Developed the Next.js frontend with SWR caching, drag-and-drop file uploads, real-time SSE timeline feeds (via @microsoft/fetch-event-source), interactive extraction verification panels with auto-save, and a results view with saved/bookmarks, action-item toggles, and history filters
⚙️

Technical Solution

  • Backend: Express 4, Node.js, TypeScript, raw pg connection pool, Zod-validated env schema, Custom RS256 JWT Auth (argon2 + httpOnly cookies + sid session tracking)
  • OCR service: Python 3 + Docling + RapidOCR, isolated process under services/ocr-engine/, consuming from a dedicated BullMQ queue (OCR_QUEUE_NAME) and writing outbox events back to Postgres via the Supabase client
  • Queue & Workers: BullMQ backed by Redis (ioredis) — three separate queues: a Node ANALYSIS_QUEUE_NAME hot path (initialization → OCR → cleaning → structuring → completion), a Node BACKGROUND_QUEUE_NAME for chunking + embedding that runs in parallel without blocking the user, and a Python OCR_QUEUE_NAME — four separate ioredis connection factories (queue, worker, publisher, subscriber), exponential backoff retries, and idempotent job IDs derived from the analysis request
  • AI & Processing: Groq API (llama-3.3-70b-versatile) with a 25-second per-stage timeout, Tavily API restricted to .gov/.edu hostnames for grounding, askGroqJson() helper that strips markdown fences, finds the first {, and validates against the stage's Zod schema with a fallback on failure
  • Background path: chunking + embedding run on a dedicated worker process that loads the embedding model once and reuses it across jobs; chunks and embeddings are persisted to Postgres asynchronously and the hot path never waits on them — measured 30–35% faster time-to-result on the user-visible pipeline, with the same output quality
  • Real-time: @microsoft/fetch-event-source on the client communicating with custom SSE endpoints — per-client Redis subscribers, Postgres replay from Last-Event-ID on reconnect, periodic heartbeats, and an active /analysis/running-check session restore on page load
  • Frontend: Next.js 14 App Router, Tailwind CSS, SWR for caching, Framer Motion for timeline animations, drag-and-drop uploads, interactive extraction verification with auto-save, results panel with action-item toggles, bookmarks, and history filters
  • Data: Supabase Storage buckets (documents for raw uploads, parsed-documents for OCR Markdown output), Supabase Postgres for everything else with JSONB columns for extracted_content, outbox events, and pipeline result payloads
🏁

Outcome

Achieved a highly resilient multi-runtime pipeline that survives process restarts and network drops without losing user documents or analysis states, successfully bridging the gap between raw, intimidating government notices and actionable, human-readable checklists. After moving chunking and embedding off the hot path, the user-visible result returned 30–35% faster with the same output quality, and the hot-path memory footprint dropped substantially because the embedding model and chunker live in a dedicated background worker — and all of it with a Python OCR service, a Node hot path, and a Node background path sharing one outbox without ever claiming each other's jobs.

📊

Proof Points

  • Implemented a deterministic transactional outbox pattern via Postgres LISTEN/NOTIFY + polling fallback, with FOR UPDATE SKIP LOCKED for safe horizontal dispatcher scaling
  • Split the pipeline into 8 atomic preprocessing stages + 5 LLM stages across three isolated BullMQ queues (hot path, background path, Python OCR) — every stage can crash and restart independently without losing state
  • Moved chunking + embedding off the user-visible hot path to a parallel background worker — measured 30–35% faster time-to-result on the user-visible pipeline with the same output quality
  • Reduced hot-path memory footprint by loading the embedding model and chunker in a separate worker process that no longer competes for memory with the result-producing stages
  • Isolated the Python/Docling OCR service on its own dedicated BullMQ queue (OCR_QUEUE_NAME) and shared a single outbox with the Node pipeline via the Postgres NOTIFY trigger
  • Built a lossless SSE layer that combines Redis Pub/Sub with Postgres event-log replay via Last-Event-ID plus an active session-restore on page load
  • Eliminated LLM hallucination risk by forcing every trusted_sources URL to be a subset of URLs Tavily actually returned from .gov/.edu searches
  • Built a pipeline that survives LLM schema failures by using graceful Zod fallbacks (every stage returns a safe default with needs_human_review: true)
  • Enforced a 3-layer prompt-injection defence: delimited untrusted input, explicit SECURITY WARNING in every system prompt, and ground-truth URL constraints
💡

Lessons Learned

  • Atomicity beats batching for long pipelines. Splitting the work into small BullMQ jobs that each commit their own outbox event is what lets the system survive crashes — long-lived in-memory workers lose state on restart.
  • The outbox pattern is essential when combining Postgres with Redis queues. Direct enqueuing from API routes leads to race conditions and lost jobs; outbox + LISTEN/NOTIFY + polling fallback guarantees at-least-once delivery.
  • Separate the hot path from the background path. Not every stage is on the user's critical path. Moving chunking + embedding off the user-visible hot path returned the result 30–35% faster with the same output quality, and let the embedding model live in a separate worker that no longer fights for memory with the result-producing stages — the slow stages are still running, just not on the user's clock.
  • Isolate heterogeneous runtimes via dedicated queues. The Python Docling service and the Node workers share the same Postgres outbox but consume from separate BullMQ queues (OCR_QUEUE_NAME vs ANALYSIS_QUEUE_NAME) so they can never claim each other's jobs.
  • Model loading is a memory cost, not just a latency cost. Loading the embedding model inline on the hot path was a meaningful share of worker memory; isolating it to a dedicated background worker dropped the hot-path memory footprint substantially without any output change.
  • LLMs will fail validation eventually. Building pipeline stages with safe fallbacks (returning needs_human_review: true instead of crashing) is critical for production AI, especially on high-stakes documents.
  • Trust but verify at the schema level. Telling the LLM not to invent URLs is not enough — forcing trusted_sources to be a subset of the URLs Tavily actually returned turns prompt-engineering hope into a hard guarantee.
  • Real-time UX requires stateful backends. Relying strictly on live Pub/Sub means a 2-second client network blip loses pipeline events; storing events in Postgres and replaying via Last-Event-ID — plus an active session-restore on page load — makes reconnects invisible to the user.
  • A human-in-the-loop verification gate is worth the UX cost. Stopping the pipeline after OCR so the user can review extracted text before any LLM tokens are spent protects against OCR errors compounding into LLM hallucinations.

More Screenshots

ClearPath — AI Document Intelligence Platform screenshot 2
ClearPath — AI Document Intelligence Platform screenshot 3