Intake
Content connectors
A knowledge base is only as good as what is in it, and most of what a team knows already lives somewhere else — Confluence spaces, SharePoint sites, Slack channels. Content connectors sync those sources into the same documents, chunks, and entity graph everything else runs on: read-only, incremental, resumable, and workspace-scoped under FORCE-RLS. These are distinct from data connectors, which run governed live SQL against operational databases — content connectors ingest documents, not rows.
One contract, four sources
Every source implements the same connector interface — the taxonomy is borrowed from Onyx (the MIT-licensed open-source connector suite), so governance, provenance, and citations are identical no matter where a document came from:
Source types
Resumable, incremental sync
The sync_state jsonb on each source is the resumability contract, modeled on Airbyte's STATE message: after every document in a yielded page is durably written, the runner checkpoints that page's position — a SharePoint deltaLink, a Confluence CQL watermark, per-channel Slack cursors, or an opaque checkpoint blob. If one document fails, the checkpoint does not advance; a retry replays any earlier successful upserts idempotently instead of skipping the failed record. Every run appends a sync_runs row — an append-only ledger of kind (full · incremental · prune · permissions), timing, and stats (docs seen / added / updated / tombstoned).
No overlapping syncs
(workspaceId, sourceId). It spans the complete full, incremental, prune, permissions, or legacy-ingest operation — including remote I/O — and therefore serializes different queues and worker replicas. The per-document ingest-document queue remains a retry-compatible legacy/explicit entry point; normal crawls persist inline so checkpoint success means content is already durable.Ingestion: upsert, dedup, tombstone
ingestConnectorDocument is the single write path, and it reuses the exact same primitives a hand-authored document uses — saveDocument, chunk persistence, entity extraction, and embedding — so a synced document is indistinguishable from one you typed:
- Upsert by identity — documents carry a
source_idandexternal_id, and a partial unique index on(workspace, source_id, external_id)(wheresource_idis not null) makes re-sync a dedup: the second sighting of a page updates the same document rather than duplicating it. Manual and vault documents (nullsource_id) are untouched by this constraint. - Content-hash change detection — each document records a
source_versionand content hash. If the hash is unchanged since the last sync, ingestion skips re-chunking, re-extraction, and re-embedding — the expensive work — so polling a large space every few minutes costs almost nothing when little changed. - Soft-tombstone on delete —
listIdsgives the prune job the authoritative id set; anything present locally but absent upstream is soft-tombstoned, removing it from retrieval without hard-deleting the record. The run incrementsdocsTombstonedonly after a delete succeeds, so partial failures remain visible instead of inflating the completion count. A page that reappears upstream un-tombstones cleanly. - Revision and audit continuity — connector create/update/reactivation and prune deletion append the same immutable
document_revisionssummaries and audit events used by manual edits. A reactivated row keeps its document identity and advances its version, preserving the end-to-end history instead of starting a second lineage.
A content change also notifies the semantic cache: ingestion calls the cache's cacheInvalidateOnDocChange hook, so a cached answer grounded in a page that a connector just re-synced is invalidated by the same event that updated the document.
Normalization and optional extractors
Native text and Slack mrkdwn normalize locally; HTML has a tag-stripping fallback. Mammoth, Turndown, unpdf, and officeparser are availability-gated dynamic imports and are not installed by the base package. A deployment that installs a compatible extractor can process its format; without one, binary Office/PDF content is reported as a clear skip rather than silently decoded or claimed as ingested. Validate any added extractor in the deployment's own test matrix.
Credentials, egress & access
A source stores its secret as credential_enc — AES-256-GCM under the enc:v1 envelope, the same crypto the data connectors use. The API accepts a secret on create/update, stores only the ciphertext, and every read returns a masked placeholder plus a hasCredential flag; decryption happens only inside the sync runner at call time.
Connector HTTP uses the shared safe-egress client
Permission refreshes carry both acl_visibility and normalized acl_principals into each document:
- Confluencereads each page's read restrictions and records canonical email, account, group, and space principals. A page becomes
restrictedwhen the provider reports explicit user or group grants. - SharePoint reads Graph drive-item permissions and records email/user/group identities, inherited site/drive scopes, and organization-link tenant scope. The app itself uses
Sites.Selectedand requires explicit configured site/drive ids; site grants are a separate administrator action. - Retrieval enforces before ranking.SQL admits workspace-visible rows or restricted rows whose GIN-indexed principals intersect the authenticated actor's normalized principals. The actor's app user id and email are automatic. Provider user/group ids require a trusted identity/group mapping to add them; an unresolved restricted grant fails closed instead of becoming workspace-visible.
- Shared workspace API keys and named background/system jobs are explicit workspace-service authority and can read restricted workspace content. Delegated user keys resolve the owner's current actor principals and stay filtered.
Slack needs a per-customer internal app
conversations.history and conversations.replies were cut to roughly one request per minute and a handful of messages per page for new non-Marketplace apps — a shared third-party Slack app cannot sync at usable throughput. Each customer therefore installs a customer-owned internal app from a provided manifest and supplies its bot token; the sync respects the tier with cursor pagination and backoff. The setup steps are in Connect Confluence, SharePoint & Slack.Managing sources
Sources are created and monitored on the workspace Connectors page (/w/<workspace>/connectors) — add a source per type, watch sync status and the sync_runs history, trigger a manual sync, and pause or resume. Over the API:
GET/POST /api/v1/connectors— list (masked creds,data:read) and create (kb:write).PATCH/DELETE /api/v1/connectors/{id}— update config/creds or remove a source (kb:write).POST /api/v1/connectors/{id}/sync— enqueue a full or incremental sync (kb:write).
Research grounding
| Mechanism | Where it lives | Research grounding |
|---|---|---|
| fullLoad / poll / listIds / getPermissions | connectors/types.ts | Onyx (MIT) connector taxonomy — one contract across sources; permissions method optional for ACL-aware retrieval. |
| Persist-before-checkpoint (sync_state) | connectors/sync.ts | Airbyte RECORD-before-STATE ordering — every page's documents are durable before its state advances; retries replay idempotent upserts. |
| Cross-queue source serialization | connectors/sync.ts | A PostgreSQL session advisory lock keyed by workspace/source spans all sync verbs, queues, and replicas. |
| SharePoint driveItem delta | sources/sharepoint.ts | Microsoft Graph delta query + Sites.Selected — opaque deltaLink returns changes while explicit per-site grants bound application access. |
| Provider ACL expansion + retrieval filter | sources/* · authz/content-acl.ts | Remote read grants become normalized document principals; retrieval applies workspace visibility or actor-principal intersection before ranking. |
| Pinned safe HTTP egress | network/safe-fetch.ts | Validate and pin every DNS/redirect hop; provider-origin allowlists and credential stripping prevent redirect-based token forwarding. |
| Confluence CQL lastModified polling | sources/confluence.ts | Confluence CQL + v2 cursor pagination — poll by last-modified watermark. |
| Per-customer Slack internal app | sources/slack.ts | Slack 2025 non-Marketplace rate-limit cuts to conversations.history/replies — a shared app can't sustain throughput. |
| Availability-gated normalization | connectors/normalize.ts | MIT/Apache-only runtime policy — mammoth/turndown/unpdf/officeparser are optional dynamic imports; degrade to plain text, add no deps. |
| Content-hash upsert + revisioned tombstone | ingestConnectorDocument | Idempotent sync — skip re-embed on unchanged content; revision every change/reactivation/delete; count only successful tombstones. |
Next