Architecture
System Overview
Qadra uses a NATS-centric, multi-container architecture. There is no monolithic HTTP API in Rust. Instead, each concern lives in a dedicated container, and all inter-service communication flows through NATS request-reply messaging.
The Four Containers
| Container | Language | Role |
|---|---|---|
Rust Core (qadra-core) | Rust | Data layer. Subscribes to NATS subjects, reads/writes PostgreSQL, MongoDB, Redis. No HTTP server. |
Node.js Gateway (qadra-gateway) | TypeScript/Express | HTTP boundary. JWT creation/validation, CORS, file uploads. Bridges HTTP requests to NATS. Never touches PostgreSQL. |
React SPA (web/) | TypeScript/React | Frontend. Vite dev server at :5173, built and served via nginx in production. |
Python Agent (agent/) | Python | Intelligence layer. LLM-driven workload orchestration, synthesis, research, verification. Communicates with Rust Core via NATS. |
Request Flow
Browser (:5173)
|
v
Kong (:8000) ── JWT validation, rate limiting, routing
|
v
Node.js Gateway (:4000) ── JWT create/validate, CORS, multipart uploads
|
v NATS request-reply
|
Rust Core (no HTTP port) ── PostgreSQL, MongoDB, Redis
|
v NATS request-reply (for LLM workloads)
|
Python Agent ── LLM providers (OpenRouter, Cerebras)
Why NATS Instead of Direct HTTP?
- Decoupling: Gateway knows nothing about database schemas. Rust Core knows nothing about HTTP routing.
- Horizontal scaling: Spin up N Rust Core instances; NATS distributes requests automatically.
- Uniform messaging: Auth, workloads, flows, email, admin, audit, epistemic operations all use the same transport.
- Observability: A single catch-all NATS observer captures every message for audit without modifying handlers.
Trait-Based Dependency Injection
The Rust application layer uses a ServiceContainer with trait-based DI. All external dependencies (database, cache, LLM providers, vector index) are abstracted behind traits defined in qadra-traits:
#![allow(unused)] fn main() { pub struct ServiceContainer { pub repository: Arc<dyn Repository>, pub llm: Arc<dyn LlmProvider>, pub cache: Arc<dyn Cache>, pub docs: Arc<dyn DocumentStore>, pub queue: Arc<dyn MessageQueue>, pub runtime: Arc<dyn RuntimeExecutor>, pub vector_index: Arc<dyn VectorIndex>, pub embeddings: Arc<dyn EmbeddingProvider>, // ... more trait objects } }
Why traits?
- Testability: Mock implementations in
qadra-containerfor unit tests without a real database - Swappability: Change PostgreSQL to SQLite by implementing
Repository - Clarity: Business logic expresses intent, not infrastructure concerns
Persistence Layer
Three databases serve distinct purposes:
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ PostgreSQL │ │ Redis Stack │ │ MongoDB │
│ │ │ │ │ │
│ - Source of truth│ │ - Cache │ │ - Audit traces │
│ - pgvector HNSW │ │ - Streams │ │ - NATS audit log │
│ - All domain │ │ - Pub/Sub │ │ - Decision trees │
│ tables │ │ - Session state │ │ │
└──────────────────┘ └──────────────────┘ └──────────────────┘
| Database | Purpose | Why This Choice |
|---|---|---|
| PostgreSQL | Source of truth for all state | ACID transactions, pgvector for embeddings, mature tooling, sqlx compile-time verification |
| Redis | Hot layer for performance | Sub-millisecond cache reads, pub/sub for real-time, streams for event sourcing |
| MongoDB | Audit and compliance logs | Schema-flexible for decision trees, append-only traces, natural fit for NATS audit entries |
Why not just PostgreSQL? Redis provides 10-100x faster reads for hot data (agent definitions, pipeline configs). MongoDB handles append-heavy, schema-evolving audit logs without bloating the primary database.
What Lives Where
| PostgreSQL (Source of Truth) | Redis (Hot Layer) | MongoDB (Audit) |
|---|---|---|
| All domain tables | Agent definitions (cached) | Decision trees |
| pgvector embeddings | Pipeline configs (cached) | Request traces |
| Workloads, tasks, artifacts | Session state | NATS audit log (nats_audit) |
| Tenant data, users | Event streams | Compliance logs |
| SPO triples, documents | Pub/Sub channels | |
| Agent flows, executions | ||
| Email templates, files |
Data Flow Patterns
Write Operations
All writes go to PostgreSQL first, then publish events:
Gateway → NATS → Rust Core → PostgreSQL → Redis Stream (event)
Example: Creating a workload
- Gateway receives HTTP request, validates JWT, publishes to
qadra.{tenant}.workload.do.create - Rust Core handler validates payload, inserts into
workloadstable within a transaction - Snapshots pipeline stages into
pipeline_snapshotJSONB - Publishes
workload.createdto Redis Stream - Returns response via NATS reply
Why PostgreSQL first? PostgreSQL is the source of truth. If the Redis publish fails, the workload still exists. Event delivery can be retried.
Read Operations
Reads use the cache-aside pattern:
Gateway → NATS → Rust Core → Redis Cache → miss? → PostgreSQL → populate cache
What gets cached: Agent definitions, pipeline configurations, tenant settings (read frequently, change rarely).
What doesn't get cached: Workloads, tasks (frequently updated, stale data is dangerous), artifacts (too large, accessed infrequently).
NATS Request-Reply
All service communication uses NATS request-reply. The gateway publishes a request to a NATS subject and awaits a reply:
Gateway: NATS.request("qadra.auth.login", payload) → response
Rust Core: subscribes to "qadra.auth.>" → processes → replies
NATS Subject Hierarchy
NATS is the message bus connecting all services. Subjects follow a hierarchical naming convention.
Subject Pattern Summary
| Subject Pattern | Handler | Purpose |
|---|---|---|
qadra.{tenant}.epistemic.> | Rust Core | KG queries, triples, attribution, SPO, document ingestion |
qadra.{tenant}.workload.do.> | Rust Core | Workload CRUD (create, get, list, tasks, artifacts, stage ops) |
qadra.{tenant}.flow.do.> | Rust Core | Flow CRUD and execution (create, get, list, update, delete, execute, human_input) |
qadra.{tenant}.agent.do.> | Rust Core | Agent CRUD (create, get, list, update, delete) |
qadra.{tenant}.artifact_template.do.> | Rust Core | Artifact template CRUD (create, get, list, update, delete) |
qadra.{tenant}.chat.do.> | Rust Core | Conversations and messages (create, send, list, rate) |
qadra.auth.> | Rust Core | Authentication (login, register, refresh, tokens, email verification) -- tenant-less |
qadra.email.> | Rust Core | Email operations (send templated email via SMTP2GO) -- tenant-less |
qadra.audit.> | Rust Core | Audit log queries (list NATS bus audit trail) -- tenant-less |
qadra.admin.> | Rust Core | Super admin operations (list tenants/users, create tenant, promote/demote) -- tenant-less |
qadra.> | Rust Core (observer) | Catch-all observer -> MongoDB nats_audit collection (fire-and-forget, metadata only) |
qadra.{tenant}.workload.execute | Python Agent | Full pipeline orchestration (LLM-driven) |
qadra.{tenant}.agent.query | Python Agent | Agent loop (synthesis, research, gap detection) |
qadra.{tenant}.verify.document | Python Agent | Document verification |
Auth Operations (qadra.auth.*)
Auth subjects are tenant-less (pre-authentication context):
| Operation | Subject | Description |
|---|---|---|
login | qadra.auth.login | Verify credentials, return user + tenants |
register | qadra.auth.register | Create user + tenant + membership |
refresh | qadra.auth.refresh | Validate refresh token hash |
store_token | qadra.auth.store_token | Store refresh token hash |
revoke_token | qadra.auth.revoke_token | Revoke single refresh token |
revoke_all | qadra.auth.revoke_all | Revoke all refresh tokens for user |
get_user | qadra.auth.get_user | Get user profile + tenant memberships |
list_members | qadra.auth.list_members | List members of a tenant |
invite_member | qadra.auth.invite_member | Smart invite: add directly if user exists, email invite if not |
update_member_role | qadra.auth.update_member_role | Change a member's role (owner/admin only) |
remove_member | qadra.auth.remove_member | Remove member from tenant (owner/admin only) |
accept_invite | qadra.auth.accept_invite | Accept team invite by token |
list_invites | qadra.auth.list_invites | List pending team invites |
cancel_invite | qadra.auth.cancel_invite | Cancel a pending invite |
verify_email | qadra.auth.verify_email | Verify email token, mark user verified |
resend_verification | qadra.auth.resend_verification | Invalidate old tokens, send new verification email |
get_tenant | qadra.auth.get_tenant | Get tenant details + settings (branding) |
update_tenant | qadra.auth.update_tenant | Update tenant name/settings (owner/admin) |
Admin Operations (qadra.admin.*)
Admin subjects are tenant-less. Every operation checks is_super_admin before executing.
| Operation | Subject | Description |
|---|---|---|
list_tenants | qadra.admin.list_tenants | List all tenants (paginated) |
create_tenant | qadra.admin.create_tenant | Create tenant + assign owner |
list_users | qadra.admin.list_users | List all users with tenant memberships (paginated) |
reset_password | qadra.admin.reset_password | Reset a user's password (argon2) |
switch_tenant | qadra.admin.switch_tenant | Drop into any tenant (bypasses membership) |
promote | qadra.admin.promote | Grant super admin (cannot self-promote) |
demote | qadra.admin.demote | Revoke super admin (cannot self-demote) |
stats | qadra.admin.stats | Platform stats |
Workload Operations (qadra.{tenant}.workload.do.*)
| Operation | Subject Suffix | Description |
|---|---|---|
create | workload.do.create | Create workload (snapshots pipeline stages) |
get | workload.do.get | Get workload by ID |
list | workload.do.list | List workloads (filter by status) |
create_task | workload.do.create_task | Create task for agent at stage |
complete_task | workload.do.complete_task | Mark task completed with output |
fail_task | workload.do.fail_task | Mark task failed with error |
create_artifact | workload.do.create_artifact | Store artifact produced by agent |
record_stage | workload.do.record_stage | Record stage result |
advance_stage | workload.do.advance_stage | Advance to next pipeline stage |
forwarded_ctx | workload.do.forwarded_ctx | Build forwarded context from prior stages |
list_agents | workload.do.list_agents | List active agents |
get_pipeline | workload.do.get_pipeline | Get pipeline definition |
Flow Operations (qadra.{tenant}.flow.do.*)
| Operation | Subject Suffix | Description |
|---|---|---|
create | flow.do.create | Create flow definition |
get | flow.do.get | Get flow by ID |
list | flow.do.list | List flows (paginated) |
update | flow.do.update | Update flow (name, description, flow_data, is_active) |
delete | flow.do.delete | Delete flow |
execute | flow.do.execute | Start flow execution (creates execution, runs graph) |
execution_get | flow.do.execution_get | Get execution state |
execution_list | flow.do.execution_list | List executions for a flow |
execution_cancel | flow.do.execution_cancel | Cancel running execution |
human_input | flow.do.human_input | Submit human input (resumes paused execution) |
generate | flow.do.generate | AI-generate flow graph from natural language prompt |
Email Operations (qadra.email.*)
| Operation | Subject | Description |
|---|---|---|
send | qadra.email.send | Resolve template, render, send via SMTP2GO |
template.list | qadra.email.template.list | List global + tenant email templates |
template.get | qadra.email.template.get | Get template by slug (tenant-specific, global fallback) |
template.upsert | qadra.email.template.upsert | Create/update tenant template override |
template.delete | qadra.email.template.delete | Delete tenant template override |
template.test_send | qadra.email.template.test_send | Render template, send to requesting user |
logs | qadra.email.logs | List notification audit log (paginated) |
Epistemic Operations (qadra.{tenant}.epistemic.*)
Tenant-scoped knowledge-graph operations handled by Rust Core (and qadra-epistemic-core):
| Operation | Subject Suffix | Description |
|---|---|---|
trace_claim | epistemic.trace_claim | Trace a single claim back to its supporting SPO triples |
trace_triple | epistemic.trace_triple | Chain-of-custody provenance for a triple (source document, excerpt, asserter) |
list_attribution_sessions | epistemic.list_attribution_sessions | List KG-SMILE gate-run audit sessions (filterable, paginated) (Story 104) |
get_attribution_session | epistemic.get_attribution_session | Fetch one gate run with its per-triple attributions (Story 104) |
list_conflicts | epistemic.list_conflicts | List detected contradictions in the KG |
acknowledge_conflict | epistemic.acknowledge_conflict | Mark a conflict as acknowledged |
resolve_conflict | epistemic.resolve_conflict | Resolve a conflict (record the winning triple) |
dismiss_conflict | epistemic.dismiss_conflict | Dismiss a conflict as a non-issue |
extract_financial_metrics | epistemic.extract_financial_metrics | Financial data extraction sub-agent: query SPO for an entity's metrics (AB#100) |
NATS Observer (qadra.>)
A catch-all subscriber that records every NATS message to the nats_audit MongoDB collection. Fire-and-forget via tokio::spawn. Skips qadra.audit.> subjects to avoid recursive logging. Graceful no-op if MongoDB is unavailable. Metadata only -- no payload content stored.
Orchestration Flow (Python Agent)
Client
|
v NATS: qadra.{tenant}.workload.execute
|
Python Agent (WorkloadOrchestrator)
|
+-- For each pipeline stage:
| +-- get_forwarded_context (Rust Core via NATS)
| +-- LLM selects best agent for stage
| +-- create_task (Rust Core via NATS)
| +-- LLM executes with agent persona + context
| +-- complete_task (Rust Core via NATS)
| +-- create_artifact (Rust Core via NATS)
| +-- record_stage_result (Rust Core via NATS)
| +-- advance_stage (Rust Core via NATS)
|
+-- Return WorkloadResult (success, stages, artifacts)
Flow Execution Engine
Event-driven graph executor in src/flow_executor.rs. Processes ReactFlow graph definitions:
- Start: Parse flow graph, find Start node, seed execution queue
- Loop: Pop node from queue, check predecessors complete, execute, store output, enqueue successors, persist state
- Pause: HumanInput nodes pause execution, wait for user response
- Resume: Human input submitted, merge into state, re-enqueue successors, continue loop
- End: End node reached, execution status
completed
Node types: Start, End, Agent, Condition, HumanInput, Loop, Http, ExecuteFlow, Transform, Delay, KnowledgeLookup, Research, Artifact, Extract, SpoFilter, SpoWrite, ClaimValidation.
Safety rails: MAX_TOTAL_STEPS=500, max Loop iterations=100, max Delay=300s.
SPO Pipeline Node Types
Three node types integrate the SPO knowledge store directly into flow execution:
| Node | Purpose | Config |
|---|---|---|
| SpoFilter (Story 76) | Queries the SPO store for triples (optionally by asserter) and writes them to flow state for downstream filtering. | -- |
| SpoWrite (Story 81) | Persists an agent's output as SPO triples with provenance. | content_key, asserter, source, output_key |
| ClaimValidation (Story 98) | Grounds every prior-stage claim against SPO triples in the Knowledge Core; reports supported/contradicted/not-mentioned per claim. | input_keys, output_key, asserter_filter, query_limit |
SSE Streaming (Conversation Progress)
Real-time flow execution progress streamed to the frontend via Server-Sent Events:
React (SSE consumer) <-- SSE -- Gateway (SSE endpoint) <-- NATS pub/sub -- Rust Core
|
POST /messages/stream -> subscribes to _PROGRESS.{uuid} FlowExecutor emits
returns text/event-stream forwards events as SSE node_started/completed
closes on "done" event to progress_subject
Source Code Organization
Rust Core (src/)
Rust Core has no HTTP server. It subscribes to NATS subjects and handles all data operations:
src/
├── main.rs # NATS handler spawning (all wildcard subscribers)
├── nats_service.rs # NATS message routing and handler dispatch
├── nats_observer.rs # Catch-all NATS observer -> MongoDB audit
├── config.rs # Configuration loading
├── error.rs # Error types
├── lib.rs # Library root
│
├── ingestion/ # Knowledge Core ingestion pipeline
│ ├── analyzer.rs # LLM content classification (Spo/Both/Discard)
│ ├── chunker.rs # Text chunking (pure Rust, paragraph boundaries)
│ ├── extractor.rs # LLM triple extraction with epistemic metadata
│ ├── embedder.rs # Batch embedding wrapper (chunks + SPO nodes)
│ └── service.rs # Pipeline orchestrator (IngestionService impl)
│
├── flow_executor.rs # Event-driven graph executor for agent flows
├── flow_generator.rs # AI-powered flow graph generation from prompts
│
├── email_service.rs # SMTP2GO email delivery
├── email_renderer.rs # Handlebars template rendering
├── email_logging.rs # LoggingEmailService decorator (notification audit)
│
├── tracing/ # Audit trace system (MongoDB)
│ ├── context.rs # Trace context management
│ ├── decision.rs # Decision tree types
│ ├── repository.rs # MongoDB trace repository
│ └── types.rs # Trace, Decision, Metrics structs
│
├── telemetry/ # OpenTelemetry integration
│ └── mod.rs # Init, config
│
└── db/ # Database utilities
└── mod.rs # Connection pool helpers
Crate Workspace (crates/)
crates/
├── qadra-traits/ # ALL interfaces (tiny, stable, no deps)
├── qadra-container/ # IoC container, service wiring, mock stubs
├── qadra-db-postgres/ # Repository: PostgreSQL (sqlx)
├── qadra-vector-pgvector/ # Vector: pgvector HNSW index
├── qadra-llm-openrouter/ # LLM: OpenRouter (OpenAI-compatible)
├── qadra-cache-redis/ # Cache: Redis
├── qadra-queue-redis/ # Queue: Redis Streams
├── qadra-semantic-cache-redis/ # Semantic cache: Redis with embedding similarity
├── qadra-nats/ # NATS client utilities
└── qadra-epistemic-core/ # Epistemic operations (SPO, curvature, attribution)
Key Traits (qadra-traits)
#![allow(unused)] fn main() { /// Repository - Database operations (PostgreSQL) #[async_trait] pub trait Repository: Send + Sync { async fn get_agent(&self, id: Uuid) -> Result<Option<Agent>>; async fn list_active_agents(&self, tenant_id: Uuid) -> Result<Vec<Agent>>; // ... more methods } /// AuthRepository - User/tenant/auth operations #[async_trait] pub trait AuthRepository: Send + Sync { async fn login(&self, email: &str, password: &str) -> Result<User>; async fn register_user(&self, params: RegisterParams) -> Result<(User, Tenant)>; // ... more methods } /// WorkloadRepository - Workload/task/artifact CRUD #[async_trait] pub trait WorkloadRepository: Send + Sync { async fn create_workload(&self, params: CreateWorkloadParams) -> Result<Workload>; async fn create_task(&self, params: CreateTaskParams) -> Result<Task>; // ... more methods } /// AgentFlowRepository - Flow/execution CRUD #[async_trait] pub trait AgentFlowRepository: Send + Sync { async fn create_flow(&self, params: CreateFlowParams) -> Result<AgentFlow>; async fn create_execution(&self, params: CreateExecutionParams) -> Result<FlowExecution>; // ... more methods } /// LlmProvider - Model completions #[async_trait] pub trait LlmProvider: Send + Sync { async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse>; async fn stream(&self, request: CompletionRequest) -> Result<CompletionStream>; } /// VectorIndex - Embedding storage and search #[async_trait] pub trait VectorIndex: Send + Sync { async fn upsert(&self, tenant_id: Uuid, records: Vec<EmbeddingRecord>) -> Result<()>; async fn search(&self, tenant_id: Uuid, query: Embedding, k: usize) -> Result<Vec<SearchHit>>; } }
Node.js Gateway (gateway/)
The gateway is an Express application that handles all HTTP concerns:
gateway/src/
├── index.ts # Express app, middleware setup
├── config.ts # Environment configuration
├── nats.ts # NATS client connection
├── storage.ts # S3/MinIO file operations
├── images.ts # Imaginary image processing client
├── auth/ # JWT creation, validation, middleware
├── routes/
│ ├── auth.ts # Login, register, refresh, verify email
│ ├── users.ts # User profile, avatar
│ ├── teams.ts # Team members, invites
│ ├── tenants.ts # Tenant settings, branding
│ ├── admin.ts # Super admin operations
│ ├── agents.ts # Agent CRUD (-> NATS)
│ ├── flows.ts # Flow CRUD and execution (-> NATS)
│ ├── conversations.ts # Chat/conversation operations (-> NATS)
│ ├── knowledge.ts # Knowledge ingestion (-> NATS)
│ ├── artifactTemplates.ts # Artifact template CRUD (-> NATS)
│ ├── files.ts # File upload/download (S3 + NATS)
│ ├── email.ts # Email send (-> NATS)
│ ├── notifications.ts # Email templates, logs (-> NATS)
│ ├── audit.ts # NATS audit log (-> NATS)
│ └── health.ts # Liveness and readiness checks
└── types/ # TypeScript type definitions
Gateway responsibilities:
- JWT creation (login/register) and validation (all authenticated routes)
- CORS configuration
- Multipart file uploads to MinIO (S3-compatible)
- Image processing via imaginary (avatar resize)
- HTTP-to-NATS bridging (every route publishes a NATS request and returns the reply)
- SSE streaming for real-time conversation progress
Gateway does NOT:
- Touch PostgreSQL directly
- Execute business logic
- Manage workload state
React SPA (web/)
web/src/
├── App.tsx # Router, layout, auth guard
├── main.tsx # Entry point
├── api/ # HTTP client (Axios, talks to Gateway)
├── stores/ # Zustand state management
├── pages/
│ ├── Login.tsx
│ ├── Register.tsx
│ ├── Dashboard.tsx
│ ├── Conversations.tsx / ConversationChat.tsx
│ ├── Agents.tsx / AgentEditor.tsx
│ ├── Flows.tsx / FlowBuilder.tsx
│ ├── ArtifactTemplates.tsx / ArtifactTemplateEditor.tsx
│ ├── Admin.tsx
│ ├── Settings.tsx
│ ├── TenantPicker.tsx
│ └── ...
└── components/
└── flow/nodes/ # ReactFlow custom node components
Python Agent (agent/)
The Python Agent is the intelligence layer. It handles everything that requires LLM calls: workload orchestration, synthesis, research, gap detection, and verification. It communicates exclusively with Rust Core via NATS -- it never touches PostgreSQL, MongoDB, or Redis directly.
Entry point: qadra-agent CLI command starts a FastAPI/uvicorn HTTP server and subscribes to NATS subjects for workload execution, agent queries, and document verification.
agent/src/qadra_agent/
├── main.py # Entry point (uvicorn server)
├── api.py # FastAPI HTTP endpoints
├── nats_client.py # NATS connection and request helpers (CoreClient)
├── orchestrator.py # WorkloadOrchestrator (pipeline execution)
├── llm.py # LLM provider client (OpenRouter, Anthropic, OpenAI)
├── loop.py # Agent loop (synthesis, research, gap detection)
├── research.py # Autonomous research via Exa, Tavily, Perplexity
├── ingestion.py # Document ingestion coordination
├── attribution.py # KG-SMILE attribution
├── verification.py # Document verification
├── conflict_detection.py # Contradiction detection in KG
└── config.py # Pydantic settings (QADRA_ env prefix)
WorkloadOrchestrator
The WorkloadOrchestrator class drives workload execution through pipeline stages. It subscribes to qadra.{tenant}.workload.execute via NATS and processes each workload sequentially through its pipeline.
Execution flow for each workload:
- Load workload and agents: Fetch the workload (including
pipeline_snapshot) and list all active agents from Rust Core via NATS - Sort stages: Order pipeline stages by their
orderfield - For each stage (sequential):
get_forwarded_context-- retrieve merged outputs and artifact references from all prior completed stages (Rust Core via NATS)_select_agent-- LLM picks the best agent for the stage based on agentstructure,description, and forwarded context. If only one agent exists, it is selected directly. Temperature is set to 0.0 for deterministic selection.create_task-- record the agent assignment in PostgreSQL (Rust Core via NATS)_execute_task-- build a system prompt from the agent persona (display_name, structure, stage name) and a user prompt incorporating forwarded context from prior stages, then call the LLMcomplete_task-- store the LLM output on the task (Rust Core via NATS)- If output exceeds 200 characters,
create_artifact-- persist the output as a named document artifact (Rust Core via NATS) record_stage_result+advance_stage-- persist the stage result and move the workload to the next stage (Rust Core via NATS)
- Return
WorkloadResultwith success status, stages completed count, and artifact list
Error handling: If any stage fails, the task is marked as failed via fail_task, and the orchestrator returns a partial result with the error and any artifacts produced by earlier stages.
Key design choice: The orchestrator never writes to the database directly. Every data operation is a NATS request-reply to Rust Core. This keeps the Python Agent stateless and horizontally scalable.
NATS Subjects Handled by Python Agent
| Subject | Handler | Purpose |
|---|---|---|
qadra.{tenant}.workload.execute | WorkloadOrchestrator.execute_workload | Full pipeline orchestration |
qadra.{tenant}.agent.query | Agent loop | Synthesis, research, gap detection |
qadra.{tenant}.verify.document | Verification | Document verification against KG |
NATS Callbacks to Rust Core
The CoreClient wraps NATS request-reply calls to Rust Core for all data operations:
| CoreClient Method | NATS Subject | Purpose |
|---|---|---|
get_workload | qadra.{tid}.workload.do.get | Fetch workload with pipeline snapshot |
list_agents | qadra.{tid}.workload.do.list_agents | List active agents for agent selection |
get_pipeline | qadra.{tid}.workload.do.get_pipeline | Fetch pipeline definition (fallback) |
get_forwarded_context | qadra.{tid}.workload.do.forwarded_ctx | Merged outputs from prior stages |
create_task | qadra.{tid}.workload.do.create_task | Record agent assignment |
complete_task | qadra.{tid}.workload.do.complete_task | Store task output |
fail_task | qadra.{tid}.workload.do.fail_task | Mark task failed |
create_artifact | qadra.{tid}.workload.do.create_artifact | Persist agent output as artifact |
record_stage_result | qadra.{tid}.workload.do.record_stage | Persist stage completion |
advance_stage | qadra.{tid}.workload.do.advance_stage | Move to next pipeline stage |
LLM Provider Configuration
The Python Agent uses an OpenAI-compatible client (via httpx) that routes through OpenRouter by default. Provider selection is via environment variables:
| Variable | Default | Description |
|---|---|---|
QADRA_NATS_URL | nats://localhost:4222 | NATS server URL |
QADRA_PORT | 8080 | HTTP server port |
QADRA_LLM_PROVIDER | openai | LLM provider (openai, anthropic, openrouter) |
QADRA_LLM_API_KEY | -- | LLM API key (falls back to OPENROUTER_API_KEY from root .env) |
QADRA_LLM_MODEL | google/gemini-3-flash-preview | Default LLM model |
QADRA_LLM_EMBEDDING_MODEL | text-embedding-3-small | Embedding model |
QADRA_EXA_API_KEY | -- | Exa API key for autonomous research |
QADRA_TAVILY_API_KEY | -- | Tavily API key for research |
QADRA_PERPLEXITY_MODEL | perplexity/sonar | Perplexity model for research via OpenRouter |
QADRA_MAX_RESEARCH_ITERATIONS | 3 | Maximum research iterations per query |
QADRA_ENABLE_AUTONOMOUS_RESEARCH | true | Enable autonomous research on gap detection |
All OpenRouter HTTP requests are rate-limited through a process-global semaphore (max 8 concurrent) with exponential backoff and jitter on 429 responses.
Dependencies
Python 3.10+, key packages: nats-py (NATS client), fastapi/uvicorn (HTTP API), openai (OpenAI-compatible LLM calls), anthropic (Claude API), httpx (async HTTP), pydantic-settings (configuration), tenacity (retry logic), structlog (structured logging).
Infrastructure
Docker Compose Services
| Service | Container | Port | Purpose |
|---|---|---|---|
| postgres | qadra-postgres | 5432 | PostgreSQL 16 + pgvector |
| mongodb | qadra-mongodb | 27017 | Audit traces, NATS audit log |
| nats | qadra-nats | 4222 | NATS message bus |
| qadra-core | qadra-core | 3000 (internal) | Rust Core -- NATS handlers only |
| qadra-gateway | qadra-gateway | 4000 | Node.js HTTP gateway |
| kong | qadra-kong | 8000, 8001 | API gateway, JWT validation, routing |
| minio | qadra-minio | 9000, 9001 | S3-compatible object storage |
| imaginary | qadra-imaginary | 9002 | HTTP image processing (resize, crop, format conversion) |
Observability Stack
| Service | Container | Port | Purpose |
|---|---|---|---|
| otel-collector | qadra-otel-collector | 4317, 4318, 8889 | OpenTelemetry Collector |
| loki | qadra-loki | 3100 | Log aggregation |
| prometheus | qadra-prometheus | 9090 | Metrics storage (30d retention, 5GB max) |
| grafana | qadra-grafana | 3200 | Visualization and alerting |
| cadvisor | qadra-cadvisor | 8088 | Container metrics |
Resource Limits
| Service | Memory Limit | Memory Reserved |
|---|---|---|
| postgres | 2GB | 512MB |
| mongodb | 1GB | 256MB |
| qadra-core | 1GB | 256MB |
| qadra-gateway | 256MB | 64MB |
| minio | 512MB | 128MB |
| imaginary | 256MB | 64MB |
| prometheus | 1GB | 256MB |
| grafana | 512MB | 128MB |
Kong Configuration
Kong runs in DB-less mode with declarative config at kong/kong.yml:
- JWT validation for Qadra-issued tokens
- Injects headers:
X-User-Email,X-Tenant-Id,Authorization - Routes:
/api/*to Gateway,/*to React SPA
Network
All services run on the qadra-network Docker network. Internal service discovery uses container names (e.g., postgres:5432, nats:4222, loki:3100).
Event Streams
Redis Streams for asynchronous coordination:
| Stream | Purpose | Consumers |
|---|---|---|
tasks.created | New task assignments | Workers |
tasks.progress.{id} | Progress updates | WebSocket handlers |
tasks.completed | Task finished | Stage advancement |
stages.advance | Move workload to next stage | Pipeline coordinator |
artifacts.created | New artifact produced | Notification service |
Why Redis Streams over simple Pub/Sub?
- Durability: Messages persist until acknowledged
- Consumer Groups: Multiple workers share a stream without duplicate processing
- Replay: New consumers can read historical messages
Security Model
Authentication
Two authentication methods:
JWT Tokens (Human Users):
- Access token (15 min):
{ sub, email, name, tid, tname, role, sa? } - Refresh token (7 days):
{ sub, type: "refresh" }, SHA256-hashed for DB storage saclaim present only for super admins- Gateway creates and validates JWTs; Rust Core never sees raw HTTP auth headers
API Keys (Machine-to-Machine):
- Tenant-scoped, permission-restricted, rate-limited, revocable
Authorization Flow
Browser -> Kong (validates JWT) -> Gateway (extracts claims) -> NATS (claims in payload) -> Rust Core (enforces tenant_id)
Authorization happens at the boundary:
- Kong: Validates JWT signature and expiry
- Gateway: Extracts user/tenant claims, injects into NATS payload
- Rust Core: Enforces
tenant_idscoping on every database query
Row-Level Security
Every query is scoped to a tenant at the repository layer:
#![allow(unused)] fn main() { impl Repository for PostgresRepository { async fn get_workload(&self, tenant_id: Uuid, id: Uuid) -> Result<Workload> { sqlx::query_as!(Workload, "SELECT * FROM workloads WHERE tenant_id = $1 AND id = $2", tenant_id, id ).fetch_one(&self.pool).await } } }
Defense in Depth
| Layer | What It Prevents |
|---|---|
| Kong JWT validation | Invalid/expired tokens |
| Gateway middleware | Missing authentication |
| NATS payload validation | Malformed requests |
| Repository tenant filter | Cross-tenant data access |
| Database foreign keys | Orphaned/invalid references |
Super Admin
Platform-level is_super_admin boolean on the users table. NOT a per-tenant role. Capabilities: create tenants, drop into any tenant (bypasses membership with synthetic JWT), reset passwords, promote/demote. NATS handler: qadra.admin.>.
File Storage
MinIO provides S3-compatible object storage:
- Gateway-proxied downloads: URLs are always
/api/files/{id}/download, never direct S3 links - Image processing: Avatar uploads auto-resized via imaginary (HTTP service) to 256x256 WebP
- Purpose-based validation:
avatar/logohave 2MB limit + image-only types; others 10MB default - Object key format:
tenants/{tenant_id}/{purpose}/{entity_id}{ext} - Multi-backend: Tenants can configure their own S3 endpoint via
tenants.settings.storage
Observability
Services (Rust, Node, etc.)
|
v OpenTelemetry Protocol (OTLP)
|
OTel Collector (receives, batches)
|
+-- Loki (logs)
+-- Prometheus (metrics)
|
v
Grafana (visualization + alerting)
- Grafana dashboards: Qadra Observability (
/d/qadra-overview), Container Monitoring (/d/qadra-containers) - 8 provisioned alerts: High CPU, high memory, critical memory, scrape target down, OTel export errors, container restart, high error rate, low disk space
- Structured JSON logs: All services emit to stdout with
trace_id,tenant_id,servicefields