📚 What is this & why does it exist?

REST API handler layer that translates HTTP requests into service calls. Organized as Flask blueprints by domain (kb, document, dialog, conversation, user). Owns authentication, request validation, multi-tenancy enforcement, and JSON response shaping.

💡 Real-world analogy

Think of apps/ as the front desk of a hotel. Guests (HTTP clients) walk in with requests; the front desk staff verify identity (JWT/API key auth), check booking details (request validation), and dispatch to the right department (services). They never cook the food themselves — they just relay orders accurately.

Apps Layer: HTTP & WebSocket Handlers

REST endpoints, blueprint registration, request validation, and auth middleware that bridges HTTP clients to RAGFlow's business services.

Analogy: The apps/ layer is like the "front desk" of a hotel. It accepts HTTP requests, validates guest credentials (JWT tokens), checks tenant isolation, and routes requests to the appropriate service department. Requests that don't match expectations or fail auth are rejected at the desk before consuming service resources.

Purpose & Architecture

The apps/ sub-module contains all REST endpoint handlers and WebSocket routes for the RAGFlow API. It serves as the HTTP-to-domain adapter layer:

Handler Patterns & Request Flow

Typical REST Handler Structure

Each REST handler follows this pattern:

@bp_document.route('/documents', methods=['GET'])
@auth_required
async def list_documents():
    # 1. Extract user identity from request context (auth_required sets it)
    tenant_id = g.tenant_id
    user_id = g.user_id
    
    # 2. Parse request parameters
    limit = request.args.get('limit', 10, type=int)
    offset = request.args.get('offset', 0, type=int)
    
    # 3. Call service layer
    service = DocumentService()
    docs = service.list_documents(tenant_id, limit, offset)
    
    # 4. Shape response
    return jsonify({
        'success': True,
        'data': [doc.to_dict() for doc in docs],
        'total': len(docs)
    }), 200

Authentication & Auth Middleware

The auth_required decorator (from common/auth.py) performs these checks before the handler is invoked:

  1. Extract JWT: Read Authorization header, extract Bearer token.
  2. Decode JWT: Verify signature using secret key. If invalid, raise 401 Unauthorized.
  3. Load User: Query database for user by sub claim. If not found, raise 401.
  4. Load Tenant: Fetch user's default tenant_id. If tenant doesn't exist or is inactive, raise 403 Forbidden.
  5. Set Request Context: Store user_id, tenant_id, user_object in Quart's g (request-scoped storage).
  6. Verify Password Hash: For sensitive operations (account deletion, password reset), request body may include password. Verify via bcrypt before proceeding.

The tenant_id enforcement happens at TWO levels:

Request Validation Patterns

Most handlers validate request shape to prevent downstream errors:

@bp_document.route('/documents', methods=['POST'])
@auth_required
async def create_document():
    # Validate request JSON
    data = await request.json
    if not data.get('name'):
        return jsonify({'error': 'Field "name" is required'}), 400
    
    if not isinstance(data.get('name'), str):
        return jsonify({'error': 'Field "name" must be a string'}), 400
    
    max_len = 255
    if len(data['name']) > max_len:
        return jsonify({'error': f'Field "name" must be <= {max_len} chars'}), 400
    
    # Proceed with validated data
    service = DocumentService()
    doc = service.create_document(
        tenant_id=g.tenant_id,
        user_id=g.user_id,
        name=data['name'],
        description=data.get('description', '')
    )
    return jsonify(doc.to_dict()), 201

Blueprint Organization

Blueprints in apps/__init__.py are organized by resource type:

Blueprint Name Module Resource(s) Key Routes
bp_document document.py Documents GET/POST /documents, GET/DELETE /documents/{id}, POST /documents/{id}/upload
bp_dialog dialog.py Conversations GET/POST /dialogs, GET/DELETE /dialogs/{id}, POST /dialogs/{id}/message
bp_knowledge_base knowledge_base.py Knowledge Bases GET/POST /knowledge-bases, GET/DELETE /knowledge-bases/{id}
bp_chunk chunk.py Document Chunks GET /documents/{id}/chunks, GET /chunks/{chunk_id}, POST /chunks/{chunk_id}/rerank
bp_task task.py Background Tasks GET /tasks, GET /tasks/{id}, POST /tasks/{id}/cancel
bp_llm llm.py LLM Configuration GET/POST /llm-models, GET/DELETE /llm-models/{id}

Error Handling & HTTP Status Code Mapping

The apps layer converts domain exceptions to HTTP status codes. Common patterns:

try:
    service = DocumentService()
    doc = service.get_document(doc_id, tenant_id=g.tenant_id)
except DocumentNotFound:
    return jsonify({'error': 'Document not found'}), 404
except TenantMismatch:
    return jsonify({'error': 'Access denied'}), 403
except ValidationError as e:
    return jsonify({'error': str(e)}), 400
except InternalError as e:
    # Log error details
    logger.exception(f"Internal error: {e}")
    return jsonify({'error': 'Internal server error'}), 500

WebSocket Handlers & Real-Time Updates

Some handlers support WebSocket upgrade to stream real-time updates:

@bp_document.websocket('/documents/{doc_id}/progress')
@auth_required
async def stream_document_progress(doc_id):
    ws = websocket.receive()
    tenant_id = g.tenant_id
    
    try:
        doc = DocumentService().get_document(doc_id, tenant_id)
    except DocumentNotFound:
        return jsonify({'error': 'Document not found'}), 404
    
    # Subscribe to task progress updates
    task_id = doc.task_id
    
    # Stream progress from Redis every 500ms
    while True:
        progress = redis_client.get(f'task:{task_id}:progress')
        await websocket.send(json.dumps({
            'task_id': task_id,
            'progress': json.loads(progress)
        }))
        await asyncio.sleep(0.5)

Multi-Tenancy Enforcement in Apps Layer

The apps layer enforces tenant isolation at the handler level before delegating to services:

Key Files & Modules

Design Decisions & Trade-offs

The apps layer embodies several key design decisions:

Decision Rationale Trade-off
Quart async handlers High concurrency for I/O-bound requests without threading overhead Requires async/await syntax; not all libraries are async-ready
Blueprints for organization Each resource has a single blueprint, making routes modular and testable More files to manage; blueprint imports must be explicit in __init__.py
auth_required decorator Centralized auth logic, consistent across all protected routes Adds latency (JWT decode, DB query for user/tenant); can't be bypassed per-route
Tenant ID in g context Prevents accidental data leaks; tenant_id is never derived from request params Requires auth_required wrapper on every handler that needs tenant_id
Exception-to-HTTP-status mapping Clean separation of concerns; services don't know HTTP semantics Requires try/except in every handler; error messages may lose detail in transit
WebSocket for real-time Low-latency, bidirectional communication; ideal for progress streaming Connection management overhead; can't use same pool as HTTP handlers

Performance Considerations

The apps layer is designed to be lightweight and fast:

Security Implications

Future Enhancements