.# DocFlow AI — About the project

Inspiration

Every working day, a Vietnamese provincial People's Committee office (UBND tỉnh) receives hundreds of administrative documents — official letters, budget proposals, reports, emergency decisions — and has to classify each one, route it to the right department, assign it to the right person with the right security clearance, and track it to closure.

Today that work is almost entirely manual. The intake clerk reads every document, consults Decree 30/2020/NĐ-CP to pick one of 28 document types, 4 security levels, and 4 urgency tiers, then writes the routing slip by hand. A single document typically takes 5-7 days to reach approval because nobody can tell at a glance where it is in the workflow.

We heard the same three complaints over and over:

  1. ~15 minutes per document spent on clerical classification alone
  2. ~20% misrouting rate (a finance proposal lands on the legal desk)
  3. Zero realtime visibility — the chairman asks "where is document 142?" and nobody can answer without a phone call

DocFlow AI is our attempt to collapse that workflow from days to minutes.

What it does

Upload any PDF or image. In roughly 30 seconds the pipeline produces:

$$\texttt{upload} \xrightarrow{\text{OCR}} \texttt{raw_text} \xrightarrow{\text{LLM}} \texttt{classification} \xrightarrow{\text{rules}} \texttt{routed assignment}$$

  • OCR with pdfplumber for text PDFs; PaddleOCR is drop-in for scans.
  • Classification via Qwen-plus through DashScope (strong Vietnamese support), with a heuristic fallback so the demo still works without an API key. One call returns {document_type, security_level, urgency, subject_tags, summary, action_items, target_department_codes} as structured JSON.
  • Routing matches against admin-defined rules first, falls back to AI-suggested departments, and finally to the General Office so no document is ever orphaned.
  • Clearance-aware assignment picks a department head whose clearance meets the document's security level.
  • Every state change broadcasts over WebSocket so anyone watching the board sees updates without refreshing.

The product ships 4 role surfaces (Admin, Department Head, Reviewer, Submitter) with Kanban, timeline, routing-rule simulator, analytics, audit trail, and clearance-scoped views.

How we built it

Layer Tech
Backend FastAPI async, SQLAlchemy 2.0, 11 tables with cascade FKs
Async work Celery + Redis, with inline BackgroundTasks fallback
Storage Cloudflare R2 (S3-compatible) with local filesystem fallback
Realtime WebSocket manager broadcasting per-user notifications
Auth JWT access (1h) + SHA-256-hashed refresh tokens (7d), slowapi rate limiter
Frontend React 18 + Vite + TypeScript, Ant Design, @ant-design/plots
Dedupe SHA-256 of the uploaded body compared against existing candidates before write
Tests 34 — pytest unit + API-flow + testcontainers Postgres integration
Deploy Docker Compose + Caddy TLS + GitHub Actions CI/CD on a Hostinger VPS

We modelled the domain after real Vietnamese administrative practice — 6 departments, 4 roles, routing rules prioritised by document type + subject tags + security minimum, and a corpus of 7 realistic PDFs generated from the Decree 30 template (letterhead, official seal, signature block, recipient list).

Challenges we ran into

  • Dedupe race condition. Multiple parallel uploads of the same file would all pass the existence check because the query used scalar_one_or_none(), which raises on more than one match and caused the handler to fall through and create a new row. Fixed by switching to scalars().all() and comparing SHA hashes of the stored bytes.
  • File preview rendered as raw JSON. Seed documents had DB rows but no actual file on storage, so /api/documents/{id}/file returned a JSON 404 that the iframe happily pretty-printed. Fixed by fetching as a blob with the JWT attached and rendering an <Empty> component on 404.
  • Classifier misfired on header text. It was latching onto "báo cáo danh mục" inside the body of an official letter and labelling the whole doc as a report. Rewrote to prefer bare-label matches in the first 15 lines (the document header) before falling back to a body-wide scan.
  • Department heads only saw personal assignments on Kanban. A head needs to see their whole department's pipeline, not only the docs where current_assignee == self.id. Added a role-aware branch on the mine=true filter so heads get the department queue while reviewers keep the personal-only view.
  • Making the auto-generated demo video look human. Headless Playwright teleports with goto() and has no visible cursor. We injected a CSS dot that follows the mouse, used page.mouse.move(x, y, steps=40) for interpolated motion, clicked real sidebar links instead of navigating by URL, typed login forms character-by-character, and muxed Gemini 2.5 Pro TTS Vietnamese voiceover on top of the recording.

What we learned

  • Rate limits are a feature, not a bug. Our own end-to-end test kept failing at step 3 because it hit our own 5-per-minute login limit. The fix was the right lesson — any test suite that logs in as $N$ users has to respect the per-IP budget.
  • Don't backward-engineer a classifier from one PDF. Our first heuristic worked beautifully on the first sample and failed on 5 of 7 later ones. You only discover distribution drift once you add variety, so build the corpus first.
  • A demo is a product surface. Judges form their opinion in the first five minutes, so we invested in nine real demo accounts on the login page, a narrated full-flow recording, and a dashboard populated with 30 days of activity. That investment also surfaced four real bugs we would otherwise have shipped.

What's next

  • Replace the heuristic classifier with a fine-tuned Vietnamese LLM trained on labelled reviewer feedback once we have enough corrections.
  • Content-based access control — today clearance is role-enforced only; we want $\texttt{user.clearance} \ge \texttt{doc.security_level}$ enforced on every list endpoint automatically.
  • Password reset UX, not just the admin CLI.
  • Full per-document audit export (PDF + JSON) for compliance filing.

Code is open source on GitHub, live at docflow.nullshift.sh, and the full demo walkthrough is in docs/demo-video/ — including both a scripted 4:30 tour and a multi-role narrated flow where the submitter uploads a budget proposal, the department head approves it, and the admin watches it happen on the analytics dashboard.

Built With

Share this project:

Updates