Connectors & Content Ingestion
Overview
Connectors are how external content enters Qadra. A connector mirrors documents from a source system (Confluence, Google Drive, SharePoint, Slack, S3, or the gateway), feeds them through a processing pipeline, and loads the result into the vector index and SPO knowledge graph.
The design follows a Moveworks-inspired three-stage pipeline:
MIRROR (Connectors) --> PROCESS (Chunk + Enrich + Embed) --> LOAD (VectorIndex)
Everything is trait-based and swappable. Each connector implements the
ContentConnector trait; the platform composes connectors, a DocumentProcessor, an
EmbeddingProvider, and a VectorIndex behind abstract interfaces, so the ingestion
core knows nothing about any specific source system.
The human-gated approval flow for documents (propose -> review -> approve -> ingest) and the ingestion internals (content analyzer, triple extraction, chunking, provenance) are covered in the Knowledge Graph chapter. This page covers the connectors, sync, and entitlements that sit in front of that pipeline.
The MIRROR -> PROCESS -> LOAD Pipeline
| Stage | Responsibility | Trait |
|---|---|---|
| MIRROR | Pull documents from a source system, detect what changed since last sync | ContentConnector |
| PROCESS | Chunk documents, enrich chunks, generate embeddings | DocumentProcessor, EmbeddingProvider |
| LOAD | Upsert embeddings into tenant-isolated vector storage; route facts to the SPO index | VectorIndex |
The MIRROR stage produces a SyncDelta (added / modified / deleted document IDs). Only
changed documents flow downstream, so an incremental sync never re-processes the entire
corpus.
The ContentConnector Model
A connector is a streaming source of documents. The ContentConnector trait exposes two
core operations:
#![allow(unused)] fn main() { #[async_trait] pub trait ContentConnector: Send + Sync { /// List documents, optionally only those changed since a timestamp (delta sync). async fn list_documents(&self, since: Option<DateTime<Utc>>) -> Result<Vec<DocumentState>>; /// Fetch full document content (streaming, zero-copy where possible). async fn fetch_documents(&self, ids: &[String]) -> Result<DocumentStream>; } }
Connectors are registered in the ServiceContainer via a ConnectorRegistry
(HashMap<String, Arc<dyn ContentConnector>>), keyed by connector type. The supporting
DocumentProcessor (chunk / enrich / embed) and VectorIndex (semantic search) traits
complete the pipeline:
| Trait | Purpose | Key Methods |
|---|---|---|
ContentConnector | Knowledge ingestion | list_documents, fetch_documents |
DocumentProcessor | Chunking / enrichment | chunk, enrich, embed |
EmbeddingProvider | Vectors from text | embed(texts), dimension() |
VectorIndex | Semantic search | upsert, search, delete_by_document, delete_tenant |
SyncStateStore | Delta sync tracking | get_states, upsert_states, mark_deleted, get_last_sync |
Available Connectors
| Connector | Crate | Source System | Source Filter |
|---|---|---|---|
| Gateway | qadra-connector-gateway | Direct uploads via the gateway (default) | -- |
| Confluence | qadra-connector-confluence | Atlassian Confluence wiki | Spaces |
| Google Drive | qadra-connector-gdrive | Google Drive files | Folders |
| SharePoint | qadra-connector-sharepoint | Microsoft SharePoint | Sites / libraries |
| Slack | qadra-connector-slack | Slack messages | Channels |
| S3 | qadra-connector-s3 | S3-compatible object storage | Prefixes |
The gateway connector is always available (it backs user uploads). The rest are opt-in per tenant and gated by entitlements (see below).
Sync Strategy
Each connector can be driven by three sync modes:
| Type | Trigger | Method |
|---|---|---|
| Incremental | Every 15 minutes | list_documents(since=last_sync) -> delta -> fetch only changed docs |
| Full | Daily, 2 AM | list_documents(since=None) -> detect deletions, rebuild sync state |
| Webhook | Real-time | Process a single document immediately |
- Incremental sync is the common path: ask the source for everything changed since the
last successful sync, build a
SyncDelta, and process only the added/modified documents. - Full sync runs daily to catch deletions and repair drift. Because it lists the entire
corpus, it can detect documents that disappeared from the source (which incremental sync
cannot reliably see) and rebuild the
sync_statetable from scratch. - Webhook sync handles real-time updates: a single document is processed immediately on notification, without waiting for the next incremental window.
Sync history is recorded in the sync_runs table; per-document state lives in sync_state
(primary key tenant_id + connector + source_id).
Change Detection
To decide whether a document actually changed, connectors compare source metadata in a strict priority order --- cheapest and most reliable first:
- ETag comparison --- fastest, HTTP-native. If the source returns an ETag and it matches the stored one, the document is unchanged. No content fetch needed.
- Version comparison --- for systems with explicit version numbers (Confluence, SharePoint). A higher version means the document changed.
- Timestamp comparison --- the fallback. Compare
last_modifiedagainst the storedsynced_at.
Each document's tracked state is captured in DocumentState:
#![allow(unused)] fn main() { pub struct DocumentState { pub source_id: String, pub content_hash: String, // BLAKE3 of content pub last_modified: DateTime<Utc>, pub etag: Option<String>, pub version: Option<u64>, pub synced_at: DateTime<Utc>, } }
A BLAKE3 content_hash is the final tiebreaker: even if ETag/version/timestamp suggest a
change, an identical hash means the processed content is unchanged and downstream
re-embedding can be skipped.
Zero-Copy Processing
Ingestion is engineered to avoid unnecessary allocations as content flows from source to vector store:
DocumentContentis an enum ofMmap,Bytes, orBorrowed--- documents can be memory-mapped or borrowed with zero allocation on load.Chunkborrows from the document content (&'doc str) --- no string copies during chunking.EnrichedChunkusesCow<'doc, str>--- it only allocates if a chunk is actually mutated during enrichment.
Supporting crates: memmap2 (memory-mapped files), bytes (zero-copy byte buffers),
bumpalo (arena allocation), blake3 (fast content hashing).
Per-Tenant Connector Configuration
Connector configuration is per-tenant, stored in the tenant_connectors table and managed
through the TenantConnectorStore trait:
| Method | Purpose |
|---|---|
get_enabled | List enabled connectors for a tenant |
get | Fetch a single connector's config |
upsert | Create or update connector config |
disable | Disable a connector |
update_credentials | Rotate credentials |
Each configuration holds:
- Credentials --- one of
OAuth2,ApiKey,Basic, orAws, encrypted at rest. - Sync config --- cron schedule, incremental interval, webhook settings, rate limits.
- Source filter --- connector-specific scoping: Confluence spaces, Google Drive folders, Slack channels, S3 prefixes.
Because credentials and source filters live per-tenant, two tenants can connect to the same source system (e.g. two Confluence instances) with completely isolated configuration and sync state.
Tenant Entitlements
Connectors and LLM providers are gated by service plans. Every tenant has an entitlement record defining what it can use and how much.
Service Plans
Three tiers --- free, pro, enterprise --- each defines:
- Limits:
max_connectors,max_documents,api_requests,llm_tokens,storage,agents,pipelines,concurrent_workloads. - Available services: which connectors and LLM providers are enabled.
Enforcement
Before any connector or service operation, the entitlement is checked:
#![allow(unused)] fn main() { // Before adding a connector / running a service operation: let ent = entitlements.get(tenant_id).await?; ent.can_add_connector(connector_type)?; // checks service availability + limits }
can_add_connector validates both that the connector type is available on the tenant's plan
and that the tenant is under its max_connectors limit.
Override Hierarchy
Entitlements resolve in priority order, most specific first:
explicit service_overrides > limit_overrides > plan defaults
A tenant on the pro plan can be granted a single extra connector via a limit_override,
or have a specific connector enabled via a service_override, without changing its plan.
Entitlement Schema
| Table | Purpose |
|---|---|
service_plans | Plan definitions (id, limits JSONB, available_services JSONB) |
tenant_entitlements | Per-tenant plan + overrides + current usage |
entitlement_audit_log | Who changed what, when, and why |
The TenantEntitlementStore trait exposes get, set_plan, set_limit_override,
enable_service, and disable_service. Admin (Qadra employee) endpoints manage entitlements
via GET/PUT/PATCH /admin/tenants/{id}/entitlements|plan|limits|services|usage|audit.
Feature-Flag Compilation
Beyond runtime entitlement checks, connectors are also gated at compile time via Cargo feature flags. A connector that is not compiled in cannot be enabled regardless of plan.
[features]
default = ["postgres", "openrouter", "redis", "mongo", "pgvector", "connector-gateway"]
minimal = ["sqlite", "ollama", "embed-local", "connector-gateway"] # zero cloud deps
enterprise = ["postgres", "openrouter", "pgvector", "redis", "mongo",
"embed-openai", "connector-confluence", "connector-sharepoint",
"connector-s3", "connector-gateway"]
full = ["..."] # everything
Two gates work together:
- Build-time ---
is_service_compiled(service)checks the active feature flags. Theminimalbuild ships with only the gateway connector and zero cloud dependencies. - Runtime ---
entitlements.get(tenant_id)checks the tenant's plan and overrides.
A connector must pass both gates to run: it must be compiled into the binary and enabled on the tenant's plan.