Overview
The RAGFlow API module is the HTTP/WebSocket adapter layer that connects external clients to the RAG (Retrieval-Augmented Generation) platform core. It implements:
- Async Web Server (Quart): High-concurrency request handling with Python async/await
- Multi-Tenant Isolation: Tenant ID filtering at query level, enforced across all data access
- Service Layer Pattern: 15+ domain services (DocumentService, DialogService, TaskService, etc.) encapsulating business logic and state mutations
- Implicit Task Queue: Redis-backed document processing with background executor coordination
- Hybrid Async+Sync: Async handlers + synchronous Peewee ORM, optimal for I/O-bound workloads
- Dual Auth Strategy: JWT tokens + session-based authentication with password hashing (bcrypt)
Architecture Diagram
(Web/Mobile/API)"] -->|HTTP/WebSocket| B["Quart Server
(Async Handler)"] B -->|Request Validation| C["Auth Middleware
(JWT Decode)"] C -->|Load Tenant| D["Multi-Tenant Context"] D -->|Route to Handler| E["REST Endpoints
(apps/)"] E -->|Business Logic| F["Service Layer
(15+ Services)"] F -->|Tenant Filtering| G["Peewee ORM
(Synchronous)"] G -->|SQL Queries| H["MySQL/PostgreSQL
(Transactional)"] F -->|Queue Task| I["Redis Queue"] I -->|Poll & Process| J["Background Executor
(Task Worker)"] J -->|Update Progress| K["Redis Cache"] B -->|Progress Stream| L["WebSocket Handler"] K -->|Fetch Status| L L -->|Push Update| A J -->|State Mutation| H M["Error Handler
(Custom Exceptions)"] -->|Convert to HTTP| E N["Connection Pool
(10-20 conns)"] -->|Absorb Contention| G
Design Philosophy
Why Async Web Server + Sync ORM?
The hybrid approach achieves high concurrency for I/O-bound workloads (uploads, API calls to external LLMs) while keeping business logic simple and debuggable:
- Quart (async Flask): Handles concurrent requests without thread-per-request overhead
- Peewee (sync ORM): Simple query API, fewer abstractions to debug, connection pool (10-20) absorbs DB contention
- Task Queue (Redis): Document processing returns immediately; progress updates via WebSocket
- Avoids async ORM complexity: Async drivers (asyncpg, motor) add significant operational burden
Multi-Tenancy Enforcement
Every domain object carries a tenant_id. Auth layer loads the user's default tenant and passes it through the entire request lifecycle:
- Queries are filtered at ORM level, not application tier
- Missing
tenant_idcheck = security vulnerability - Services are responsible for enforcing tenant isolation (critical responsibility)
Service Layer: More Than CRUD
Domain services do not merely wrap database operations. They coordinate state transitions, manage soft deletes, queue tasks, and handle cross-table mutations atomically. Bypassing a service = bypassing multi-tenancy isolation.
Sub-Modules
apps/
Blueprint routing, REST endpoints, request handlers, and auth middleware. Entry point for all HTTP requests and WebSocket connections.
db/
Peewee ORM models (15+ tables), schema definitions, and the 15+ domain services that coordinate state mutations.
services/
Detailed analysis of all 15+ domain services: DocumentService, DialogService, TaskService, KnowledgebaseService, and others.
Document Upload Lifecycle (Narrated Walkthrough)
Trace a PDF upload from HTTP request through task execution and progress reporting:
-
HTTP POST /api/documents/upload — Client sends multipart form with PDF file, document name, and knowledge base ID. Request lands in
apps/document_api.pyhandler. -
Auth Middleware Validates JWT — Token decoded in
common/auth.py. User's default tenant extracted; tenant ID passed through request context. - Handler Calls DocumentService.create() — Business logic layer receives request. Validates: tenant isolation, knowledge base ownership, file format, size limits.
- File Saved to Storage — PDF persisted to disk or cloud storage. File path stored in Document row.
-
Document Row Inserted — Peewee ORM inserts row into
documentstable. Tenant ID enforced at ORM level. -
TaskService.queue() — DocumentService delegates heavy lifting to TaskService. Creates Task row with status=
pending. - Redis Queue Entry Created — Task ID pushed to Redis list. Background executor polls this queue.
- Handler Returns 202 Accepted — HTTP response sent immediately. Document created, processing queued.
- Background Executor Polls Redis — Separate process (not blocking HTTP handler) dequeues task ID.
- Parsing & Chunking — ParsingService extracts text. ChunkService breaks PDF into semantic chunks. Embeddings generated (external LLM call).
-
Progress Updated in Redis — Executor pushes progress events:
{ "task_id": "...", "progress": 45 }to Redis. -
WebSocket Handler Fetches Status — Browser WebSocket listener polls
/api/tasks/{task_id}/statusor listens to server push. - Client Receives Progress — UI updates progress bar in real-time without blocking HTTP.
-
Task Completion — Executor marks task status=
completed. Document now searchable.
Authentication & Multi-Tenancy
JWT Token Flow
All requests require a valid JWT token issued by the identity provider:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Auth middleware in common/auth.py decodes the token and extracts:
user_id— Unique user identifiertenant_id— Default tenant for this userpermissions— Role-based access control (admin, editor, viewer)
Tenant Isolation
Multi-tenancy is enforced at three levels:
- Auth Layer: User's tenant ID loaded from token
- ORM Layer: All queries filtered by
WHERE tenant_id = ? - Service Layer: Services validate tenant ownership before state mutations
Password Hashing
Passwords (if used for local auth) are hashed with bcrypt in apps/auth/password.py:
import bcrypt
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
bcrypt.checkpw(password.encode(), hashed) # verify
Architectural Decisions
| Decision | Alternative | Rationale |
|---|---|---|
| Async Web Server (Quart) | Synchronous Flask | Handle 100+ concurrent uploads without thread-per-request overhead. Optimal for I/O-bound workloads. |
| Synchronous Peewee ORM | Async ORM (asyncpg, motor) | Simpler to debug, fewer abstractions. Connection pool absorbs DB contention. Avoids async driver complexity. |
| Service Layer Pattern | Direct ORM queries in handlers | Isolate business logic, coordinate state transitions atomically, enforce multi-tenancy consistently. |
| Implicit Task Queue (Redis) | Celery / RabbitMQ | Simpler operations (no external broker), Redis widely deployed. Tradeoff: manual retry/dead-letter handling. |
| Tenant ID at Query Level | Application-tier filtering | Prevent data leaks from missed filters. ORM-level enforcement is harder to bypass accidentally. |
| Custom Peewee Fields (JSONField, ListField) | JSON columns (native DB types) | Portability across MySQL/PostgreSQL. Custom serialization logic stays in Python, not SQL. |
| Deadlock Retry Pattern | Fail on first deadlock | MySQL InnoDB deadlocks under high concurrency. Exponential backoff retry improves reliability without re-architecting transactions. |
| WebSocket for Real-Time Progress | Polling / Server-Sent Events | Bidirectional communication. Lower latency than polling. More efficient than HTTP long-polling. |
Directory Structure
api/
├── apps/ # Blueprint routing and REST handlers
│ ├── __init__.py # Blueprint registration
│ ├── auth/ # JWT decode, password hashing
│ ├── document_api.py # /api/documents/* endpoints
│ ├── dialog_api.py # /api/dialogs/* endpoints
│ ├── task_api.py # /api/tasks/* endpoints (status polling)
│ └── websocket/ # WebSocket handlers for real-time updates
├── db/
│ ├── db_models.py # Peewee ORM models (15+ tables)
│ ├── services/ # 15+ domain services
│ │ ├── document_service.py
│ │ ├── dialog_service.py
│ │ ├── task_service.py
│ │ ├── llm_service.py
│ │ └── ...
│ └── joint_services/ # Cross-domain services
│ ├── user_account_service.py
│ ├── memory_message_service.py
│ └── tenant_model_service.py
├── common/
│ ├── auth.py # Auth decorators, JWT decode
│ ├── exceptions.py # Custom exception classes
│ └── utils.py # Shared utility functions
└── utils/
├── file_ops.py # File I/O helpers
├── serialization.py # JSON/pickle serialization
└── logging.py # Custom logging setup