All work

Personal project · sole engineer · 2026

DocFlow

Long-running AI document analysis shouldn’t block, or break, the API that receives the documents.

Stack

FastAPICelery + RedisPostgreSQL
View code on GitHub

Architecture

01 / Context

OCR and model calls take seconds to minutes; an HTTP request shouldn’t. DocFlow is a reusable reference implementation of the asynchronous pattern I ran in production at IFAD, rebuilt from scratch in the open.

02 / What I built

A FastAPI backend where users upload PDFs, DOCX files or images and ask for a summary, invoice extraction or contract metadata. The upload returns straight away; Celery workers on Redis do the heavy work, and PostgreSQL tracks every request and attempt.

  • JWT authentication, per-user documents and files, and email-based password reset.
  • Originals stored in MinIO (S3-compatible); metadata, requests, job attempts and results in PostgreSQL, with Alembic migrations.
  • Text extraction with pypdf, python-docx and Tesseract OCR, then OpenAI for the requested analysis.
  • Everything runs locally with Docker Compose: API, worker, PostgreSQL, Redis and MinIO.

03 / Engineering decisions

The questions a reviewer should ask of any job system, and how DocFlow answers them today:

  • Why Celery and Redis? A mature Python task queue with retries and backoff built in, and Redis doubles as broker and result backend, so the local stack stays small.
  • How does the client know a job finished? Each request moves QUEUED → PROCESSING → COMPLETED or FAILED in PostgreSQL. Clients poll the status endpoint; the result endpoint returns 404 until the result exists.
  • How are duplicate results prevented? The database allows exactly one stored result per processing request, and every attempt is recorded with its attempt number.
  • What happens when a worker dies mid-job? Today the request stays in PROCESSING: tasks are acknowledged when received, so a crashed worker’s task isn’t redelivered. That is the first thing on the list below.

04 / What I’d harden next

Written down deliberately. These are the gaps between a working reference and a production service:

  • Late acknowledgement (acks_late + reject_on_worker_lost) plus a sweeper that re-queues requests stuck in PROCESSING.
  • Let failures reach Celery’s retry policy: the task currently records a failure instead of re-raising it, so its declared retries never fire.
  • Idempotency keys on upload, so a client retrying a slow request can’t create a second job.
  • Push completion (webhook or server-sent events) instead of polling, and validated structured output for invoices and contracts.

Next project

AI Workspace at IFAD