Table of Contents
Executive Summary
Architecture Diagram
Architecture Principles
System Modes
Service Inventory
SignalR Hubs
Communication Patterns
Data Stores
Executive Summary
TENGWAR is an on-premises enterprise RAG (Retrieval-Augmented Generation) platform designed for single GPU node deployment. It combines a .NET 10 backend with Python microservices, five vLLM model servers (chat, RAG-helper auxiliary, embedding, reranker, GLM-OCR), a background memory worker, and three client surfaces -- all orchestrated through Docker Compose as 18 Docker services (17 long-running + 1 init).
The platform provides:
Three client surfaces: Windows desktop (MAUI Blazor Hybrid), macOS desktop (Mac Catalyst), and a web fallback (Blazor Server).
17-phase document ingestion pipeline (schema 2.0): Docling parsing, GLM-OCR vision, chunking, embedding, and vectorization coordinated via RabbitMQ.
Hybrid retrieval: Dense + BM25 sparse + visual search with RRF fusion, cross-encoder reranking, and context budget allocation.
Agent-driven chat: Microsoft Agent Framework with autonomous tool invocation, streaming responses via SignalR, and a middleware chain for security, audit, and citation capture.
Security-aware retrieval: RBAC, security clearance, and department-based document filtering propagated into the retrieval layer.
Mode-based orchestration: System modes (Online, DocumentProcessing, Transitioning, Updating) gate request handling at the HTTP level without stopping containers.
All 18 Compose services share a single Docker bridge network (app-network) except for the host-network UDP discovery sidecar. GPU-backed model services share a single NVIDIA GPU via vLLM's --gpu-memory-utilization 0.01 + --kv-cache-memory-bytes configuration.
Architecture Diagram
flowchart TB
desktop[Windows / macOS desktop<br/>MAUI Blazor Hybrid] -->|HTTPS + SignalR| api[Backend API<br/>.NET 10<br/>Ports 5000 / 5001]
web[Web fallback<br/>Blazor Server frontend] -->|HTTPS + SignalR| api
api --> pg[(PostgreSQL<br/>metadata, auth, state)]
api --> mq[(RabbitMQ<br/>async jobs)]
api --> minio[(MinIO<br/>object storage)]
api --> qdrant[(Qdrant<br/>vector DB)]
api --> searx[SearXNG<br/>web search]
mq --> ingester[Ingester<br/>17-phase Docling pipeline (schema 2.0)<br/>Port 8108]
ingester --> tokenizer[Tokenizer<br/>BM25 sparse vectors<br/>Port 8104]
ingester --> translator[Translator<br/>M2M-100 language service<br/>Port 8105]
ingester --> embedding[Embedding<br/>BGE-M3<br/>Port 8101]
ingester --> ocr[GLM-OCR<br/>vision OCR<br/>Port 8103]
api --> chat[Chat vLLM<br/>Qwen3.6 35B-A3B FP8<br/>Port 8100]
api --> raghelper[RAG-helper vLLM<br/>Qwen3.5-4B FP8<br/>Port 8000 internal / 8106 dev]
api --> reranker[Reranker<br/>BGE-Reranker-v2-m3<br/>Port 8102]
api --> memory[Memory worker<br/>per-user extraction]
mq --> memory
memory --> pg
memory --> qdrant
minio --> init[minio-init<br/>one-shot bucket setup]Architecture Principles
1. Contracts-First Boundaries
The Contracts/ project is the shared schema source of truth. All DTOs, requests, responses, configuration interfaces, enums, and authorization constants live there (~390 files). Any schema change must be synchronized across all consumers -- backend, desktop, and web.
2. Layered Backend Design
The backend follows clean architecture: Api (controllers, hubs, background services, middleware) depends on Application (interfaces, service contracts), which depends on Domain (entities, enums, value objects). Infrastructure (implementations, EF Core, agent tools) depends on Application and Domain but never on Api.
3. Asynchronous Ingestion
Document processing is fully asynchronous. Uploads are queued via RabbitMQ, processed by the ingester through a 17-phase pipeline (schema 2.0), and results flow back through RabbitMQ progress messages. The backend broadcasts progress in real time via SignalR.
4. Security-Aware Retrieval
Every retrieval query is filtered by the requesting user's RBAC role, security clearance level, and department assignment. The security context is extracted from JWT claims by middleware and propagated into the retrieval pipeline.
5. Operational Resilience
All services include Docker health checks with configurable intervals and start periods. The backend runs health monitoring, auto-recovery, heartbeat timeout detection, and document state reconciliation as background services. Mode transitions use a semaphore and SignalR broadcast pattern for safe coordination. Failure scenarios and operator procedures live in the canonical runbook: `15-operations-runbook.md` §Failure Scenarios.
6. Cross-Platform Parity
All UI components, pages, services, and state management live in a shared Blazor RCL (Tengwar.Ui). Desktop-specific services (mDNS discovery, folder sync, TLS trust, secure storage) live in Tengwar.Desktop. Each host project (Windows, macOS, Web) is a thin shell that composes these shared layers.
System Modes
The system uses mode-based request blocking -- all containers run continuously, and the backend gates chat requests at the HTTP level based on the current mode.
| Mode | Behavior | |------|----------| | Online | Normal operation. Chat requests are served. | | DocumentProcessing | Ingestion is active. Chat requests are blocked with HTTP 409 Conflict. | | Transitioning | Mode change in progress. All requests are held until the mode settles. | | Updating | System update in progress (image pulls, container restarts). Requests blocked. See `13-update-system.md`. |
Transition Flow
Acquire semaphore -- prevents concurrent mode transitions.
Set `Transitioning` -- database flag updated.
SignalR broadcast --
SystemHubnotifies all connected clients of the pending transition.Drain in-flight requests --
ChatRequestRegistrywaits for active chat requests to complete.Set target mode -- database flag updated to the final mode (
OnlineorDocumentProcessing).SignalR broadcast -- clients receive the final mode.
Auto-Transitions
Upload triggers DocumentProcessing: When documents are uploaded, the system transitions to
DocumentProcessingmode automatically.Batch completion triggers Online:
BatchCompletionBackgroundServicepolls every 15 seconds. When all documents in a batch are processed, it transitions back toOnline.DocumentProgressBackgroundServicealso monitors RabbitMQ events for faster detection.
Service Inventory
18 Docker services: 17 long-running + 1 init container (minio-init).
| Service | Image / Build | Dev Port (host:container) | Role | Health Check | |---------|---------------|----------------------|------|-------------| | rabbitmq | rabbitmq:4.3.1-management-alpine | 5672:5672, 15672:15672 | Message broker for async document processing | rabbitmq-diagnostics -q ping | | qdrant | qdrant/qdrant:v1.18.2 | 6333:6333, 6334:6334 | Vector database for dense, sparse, and visual embeddings | TCP check on port 6333 | | ingester | Build: infra/docker/Dockerfile.Ingester | 8108:8000 | 17-phase document processing pipeline (schema 2.0; Docling + GLM-OCR) | HTTP GET /health | | tokenizer | Build: infra/docker/Dockerfile.Tokenizer | 8104:8000 | BM25 sparse vector tokenization for hybrid search | HTTP GET /health | | translator | Build: infra/docker/Dockerfile.Translator | 8105:8001 | Language detection and translation (M2M-100 1.2B) | HTTP GET /health | | postgres | postgres:17.10-alpine | 5432:5432 | Relational database for metadata, auth, state, and settings | pg_isready | | minio | quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z | Variable (env) | Object storage for documents, processed artifacts, generated files | HTTP /minio/health/live | | minio-init | quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z | None | One-shot init: creates MinIO buckets at first start | None (exits after completion) | | chat | Build: infra/docker/Dockerfile.Qwen36Chat | 8100:8000 | vLLM chat LLM (Qwen/Qwen3.6-35B-A3B-FP8) | HTTP GET /health | | rag-helper | Build: infra/docker/Dockerfile.RagHelper | 8106:8000 (dev) | vLLM auxiliary LLM (Qwen3.5-4B FP8) for HyDE, Self-RAG reflection, and query expansion -- offloads small-model calls from the main chat container. See `rag-helper-auxiliary-generation.md`. | HTTP GET /health | | embedding | Build: infra/docker/Dockerfile.BgeEmbedding | 8101:8000 | vLLM BGE-M3 embedding model (1024 dimensions) | HTTP GET /health | | reranker | Build: infra/docker/Dockerfile.BgeReranker | 8102:8000 | vLLM BGE-Reranker-v2-m3 cross-encoder reranker | HTTP GET /health | | glm-ocr | Build: infra/docker/Dockerfile.GLMOCR | 8103:8000 | vLLM vision OCR model (GLM-OCR 0.9B) | HTTP GET /health | | backend | Build: infra/docker/Dockerfile.Backend | 5000:5000, 5001:5001 | .NET 10 API: orchestration, auth, REST, SignalR, agent framework | ASP.NET health endpoints | | frontend | Build: infra/docker/Dockerfile.Frontend | 8080:8080, 8443:8443 | Blazor Server web fallback (hosts shared Tengwar.Ui RCL) | HTTP GET /_framework/blazor.server.js | | memory-worker | Build: infra/docker/Dockerfile.MemoryWorker | None | Background per-user memory extraction consumer | HTTP GET /health | | discovery | Build: infra/docker/Dockerfile.Discovery | 40123/udp (host network) | LAN broadcast discovery beacon for desktop clients | None (UDP responder) | | searxng | searxng/searxng:latest | None (internal only) | Self-hosted metasearch engine for agent web search tool | HTTP GET / |
Startup Order
Services start in dependency order enforced by Docker Compose depends_on with condition: service_healthy:
Data stores:
postgres,qdrant,rabbitmq,minio(independent, start in parallel)Init:
minio-init(depends onminio)Python services:
tokenizer,translator(independent)AI models:
chatfirst (GPU memory reservation), thenrag-helper,embedding,reranker(all depend onchat)AI models (parallel):
glm-ocr(independent of other models)Ingester: depends on
rabbitmq,minio,tokenizer,translator,qdrant,embedding,glm-ocrMemory worker: depends on
postgres,rabbitmq,qdrant,minio,chat, andembeddingBackend: depends on all API-facing dependencies
Frontend: depends on
backendSearXNG and discovery: independent sidecars
SignalR Hubs
Five SignalR hubs provide real-time communication between the backend and connected clients. All hubs require JWT authentication and are mapped under /hubs/.
| Hub | Path | Purpose | |-----|------|---------| | StatusHub | /hubs/status | System status, processing progress, health updates, statistics, mode changes, and syncing-knowledge broadcasts. Buffer: 16 KB. | | ChatHub | /hubs/chat | Streaming chat tokens to user-specific groups. Each user receives tokens in their own group (chat:{userId}). Buffer: 64 KB. | | DocumentHub | /hubs/document | Document processing progress updates and group management. Clients join/leave document-specific groups (doc-progress:{batchId}). Buffer: 16 KB. | | SystemHub | /hubs/system | Real-time system metrics (CPU, GPU, memory, disk), health alerts, and admin-only service status updates. Supports subscription-based groups: SystemMetrics, HealthAlerts, AdminUpdates. Buffer: 32 KB. | | MemoryHub | /hubs/memory | Per-user memory change notifications (memory subsystem). |
SignalR Configuration
Maximum receive message size: 1 MB (default is 32 KB)
Client timeout interval: 5 minutes (for bulk import scenarios with 10000+ files)
Keep-alive interval: 30 seconds
Handshake timeout: 30 seconds
User identity resolved from JWT
NameIdentifierclaim viaNameUserIdProvider
Communication Patterns
REST (Client to Backend)
All client surfaces communicate with the backend via REST over HTTPS. The API supports versioned (api/v{version}/...) and unversioned (api/...) route prefixes. Responses follow the ApiResponse hierarchy defined in Contracts/.
SignalR (Real-Time Push)
The backend pushes real-time updates to connected clients via five SignalR hubs. Use cases include streaming chat tokens, document processing progress, system mode changes, health alerts, and metrics. SignalR connections authenticate via JWT tokens passed as query parameters.
RabbitMQ (Async Document Processing)
Document processing uses a producer-consumer pattern over RabbitMQ:
Backend publishes processing requests when documents are uploaded.
Ingester consumes requests and processes documents through the 17-phase pipeline (schema 2.0).
Ingester publishes progress messages back to RabbitMQ.
Backend consumes progress messages via
DocumentProgressBackgroundServiceand broadcasts via SignalR.
Additional queues handle cancellation, heartbeat, and dead-letter scenarios.
HTTP (Backend to AI Services)
The backend communicates with AI services (chat, rag-helper, embedding, reranker, tokenizer, translator, SearXNG) via HTTP over the Docker bridge network. All outbound HTTP calls propagate the X-Correlation-Id header via CorrelationIdDelegatingHandler.
Data Stores
PostgreSQL (Relational)
Image:
postgres:17.10-alpinePurpose: Document metadata, user accounts, authentication state, system settings, mode flags, chat session history, approval workflows, sync state, agent audit logs.
Performance tuning:
shared_buffers=256MB,effective_cache_size=512MB,work_mem=16MB,max_connections=25.Access: Backend connects via EF Core with connection string from environment variables.
Qdrant (Vector)
Image:
qdrant/qdrant:v1.18.2Purpose: Dense embeddings (BGE-M3, 1024 dimensions, L2-normalized), BM25 sparse vectors, and optional visual embeddings for hybrid search.
Access: Backend uses
QdrantVectorServicevia the Qdrant REST/gRPC API (ports 6333/6334).
MinIO (Object Storage)
Image:
quay.io/minio/minio:RELEASE.2025-09-07T16-13-09ZPurpose: Three buckets -- documents (originals), processed (artifacts/thumbnails), and generated documents (Word/Excel/PDF output from chat). The
minio-initcontainer creates these buckets on first start.Access: Backend uses presigned URLs for client downloads. Ingester accesses MinIO directly for document retrieval during processing.
RabbitMQ (Message Queues)
Image:
rabbitmq:4.3.1-management-alpinePurpose: Async coordination between backend and ingester. Queues include document processing requests, progress updates, cancellation signals, heartbeats, and dead-letter handling.
Management UI: Available on port 15672 for queue inspection and diagnostics.
Last verified against commit `2a1034c` (2026-05-17).