Skip to documentation

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:

the Connector contractts
interface Connector {
  // Backfill everything, resumable from a checkpoint.
  fullLoad(checkpoint: Checkpoint | null): AsyncGenerator<DocBatch, Checkpoint>;
  // Incremental: only what changed in [start, end).
  poll(start: Date, end: Date): AsyncGenerator<DocBatch>;
  // Prune: the authoritative id set, to detect deletions.
  listIds(): AsyncGenerator<string[]>;
  // Per-document principals, normalized by the source permission adapter.
  getPermissions?(ids: string[]): Promise<DocPermissions[]>;
}

// Every batch yields normalized documents:
type Document = {
  sourceId: string; externalId: string; url: string | null; title: string;
  markdown: string; mimeType: string; sourceVersion: string | null;
  contentHash: string;
  aclVisibility?: "workspace" | "restricted";
  aclPrincipals?: string[]; updatedAt: Date;
};

Source types

confluenceCQL polling · v2 cursorPolls by lastModified via CQL; paginates with the v2 API cursor. sync_state holds the CQL watermark so incremental syncs only fetch pages edited since the last run.
sharepointMS Graph driveItem deltaUses the Microsoft Graph delta query on driveItems; sync_state stores the opaque deltaLink, so each incremental call returns exactly what changed since the last token — additions, edits, and deletes.
slackper-customer internal app · cursor historyA bot token from a customer-owned internal app; walks conversations.history/replies with cursor pagination, one cursor per channel in sync_state. Slack mrkdwn is transformed to markdown locally.
markdown_uploadbulk import, as a connectorWraps the existing bulk-import path in the connector contract for parity — so an uploaded folder gets the same upsert, tombstone, and provenance semantics as a live source.

Resumable, incremental sync

One cross-queue advisory lock serializes every source-level operation. A crawl persists every document in a page before advancing that page's checkpoint; replay is idempotent by (source_id, external_id), and unchanged content skips re-embedding.

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

Queue singleton keys are queue-local, so the runner also takes a PostgreSQL session advisory lock keyed by (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_id and external_id, and a partial unique index on (workspace, source_id, external_id) (where source_id is 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 (null source_id) are untouched by this constraint.
  • Content-hash change detection — each document records a source_version and 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 deletelistIds gives 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 increments docsTombstoned only 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_revisions summaries 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

Confluence, SharePoint, and Slack calls are time- and size-bounded. Every hostname is resolved once per hop, the complete DNS answer set must be public, and the connection is pinned to that validated set while keeping the original TLS hostname. Redirects are handled manually and revalidated. Provider API pagination is same-origin; SharePoint file-download redirects may cross origin only after bearer/signature/cookie headers are stripped. Private, loopback, link-local, metadata, mixed public/private DNS, excess redirects, and unbounded request bodies fail before content is accepted.

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 restricted when 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.Selected and 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

Following Slack's 2025 tightening of API rate limits for non-Marketplace apps — 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

MechanismWhere it livesResearch grounding
fullLoad / poll / listIds / getPermissionsconnectors/types.tsOnyx (MIT) connector taxonomy — one contract across sources; permissions method optional for ACL-aware retrieval.
Persist-before-checkpoint (sync_state)connectors/sync.tsAirbyte RECORD-before-STATE ordering — every page's documents are durable before its state advances; retries replay idempotent upserts.
Cross-queue source serializationconnectors/sync.tsA PostgreSQL session advisory lock keyed by workspace/source spans all sync verbs, queues, and replicas.
SharePoint driveItem deltasources/sharepoint.tsMicrosoft Graph delta query + Sites.Selected — opaque deltaLink returns changes while explicit per-site grants bound application access.
Provider ACL expansion + retrieval filtersources/* · authz/content-acl.tsRemote read grants become normalized document principals; retrieval applies workspace visibility or actor-principal intersection before ranking.
Pinned safe HTTP egressnetwork/safe-fetch.tsValidate and pin every DNS/redirect hop; provider-origin allowlists and credential stripping prevent redirect-based token forwarding.
Confluence CQL lastModified pollingsources/confluence.tsConfluence CQL + v2 cursor pagination — poll by last-modified watermark.
Per-customer Slack internal appsources/slack.tsSlack 2025 non-Marketplace rate-limit cuts to conversations.history/replies — a shared app can't sustain throughput.
Availability-gated normalizationconnectors/normalize.tsMIT/Apache-only runtime policy — mammoth/turndown/unpdf/officeparser are optional dynamic imports; degrade to plain text, add no deps.
Content-hash upsert + revisioned tombstoneingestConnectorDocumentIdempotent sync — skip re-embed on unchanged content; revision every change/reactivation/delete; count only successful tombstones.

Next

Wire up your first source in Connect Confluence, SharePoint & Slack. Everything synced is searchable and citable exactly like a posted document — and feeds the same grounded answers.