.# 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:
- ~15 minutes per document spent on clerical classification alone
- ~20% misrouting rate (a finance proposal lands on the legal desk)
- 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
pdfplumberfor 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 toscalars().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}/filereturned 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 themine=truefilter 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, usedpage.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
- alembic
- ant
- api
- axios
- celery
- cloudflare
- dashscope
- design
- docker
- fastapi
- paddleocr
- postgresql
- python
- qwen-plus
- r2
- react
- recharts
- redis
- sqlalchemy
- typescript
- vite
- websocket
- zustand


Log in or sign up for Devpost to join the conversation.