Module references
Intake
Read-only sync of external sources into the corpus. Intake is the module that turns a Confluence site, a SharePoint drive, a Slack workspace, or a batch of staged markdown into deduplicated, incrementally re-synced documents rows — each one carrying where it came from and what permissions it had there.
What it does
A connector source is a governed, workspace-scoped record: a type, non-secret config, an encrypted credential, and a resumable checkpoint. You register sources through the API (or your own UI), then trigger a sync; the crawl runs server-side in a worker, page by page, persisting its checkpoint after every page so a crash resumes rather than restarts.
Ingestion is idempotent by construction: documents are keyed on (source_id, external_id), and a document whose sourceVersion is unchanged is skipped outright — no re-chunk, no re-embed. Intake is strictly read-only against the remote system: the connector interface exposes only fullLoad, poll, listIds, and an optional getPermissions. Nothing writes back to the source.
Four source types
confluence, sharepoint, slack, and markdown_upload. The first three require a structured credential; markdown_upload is credential-less and reads files staged in its own config.documents array, flowing through the same normalize → chunk → persist path as a live connector.Use it standalone
Adopting Intake alone means: an API key, one package import, and a worker process that drains the sync queue. Nothing else in the product has to be configured.
Give the key the right scopes
Reads (
GET /api/v1/connectors,GET /api/v1/connectors/{id}) require scopedata:read. Everything that changes state — create, update, delete, and triggering a sync — requires scopekb:write. There is no connector-specific scope; the connector-specific granularity lives in capabilities.A workspace key (
eli_sk_) is checked on scope alone. A delegated user key (eli_uk_) is additionally narrowed against the owner's live capabilities:connectors:readfor the reads,connectors:writefor create/update/delete, andconnectors:executefor the sync trigger. A missing capability is a 403 witherror: "insufficient_capability".Import exactly one entry point
React:
@eli-ai/react/connectors. Headless:@eli-ai/client/connectors. Types only:@eli-ai/contracts/connectors. None of the three pulls in another module's client, contracts subpath, or components.Run a worker
The sync endpoint returns
202and stops there — it validates the source, checks it is not paused, and enqueues. If nothing is draining the queue, sources stay atlastSyncedAt: nullforever and nosync_runsrow is ever written. This is the one out-of-process dependency Intake genuinely has.
What Intake does NOT require
React components
@eli-ai/react/connectors exports exactly one component, EliConnectors, plus the types EliConnectorsProps, EliConnectorsLabels, and a re-export of ConnectorSyncKind. It is a client component ("use client") and reads its transport from EliProvider.
EliConnectors
Renders a titled surface containing a card grid — one card per connector source. Each card shows the source name (config.name when set, otherwise the type with underscores replaced by spaces), a status badge, a Last synced line, a credential badge, and the source's ACL-visibility badge. When canSync is on, each card also gets a kind picker (incremental / full) and a Sync button; the button is disabled while a sync is being queued, when the source is paused, and when the source has no stored credential. On success it refetches the list and calls onSyncQueued. It also renders its own loading, error, and empty states.
EliConnectors props
No emptyLabel prop
EliConnectors takes Omit<CommonSurfaceProps, "emptyLabel"> — the empty-state text is labels.empty, not a top-level prop. Passing emptyLabel is a type error.EliConnectorsLabels (all keys required on the full type; pass any subset)
What this component deliberately does not do
connectors.list() and connectors.sync(id, { kind }). Create/update/delete is your UI driven through onConfigure plus the HTTP API below; history comes from GET /api/v1/connectors/{id}. That is a deliberate boundary — credential entry is left to the host application.Headless, with your own markup
The same client the component uses is importable directly. Every method takes an optional EliCallOptions (workspaceId, signal, headers).
HTTP API
Six operations under /api/v1/connectors. Every one is workspace-scoped: the key resolves a workspace (optionally selected with the X-Workspace-Id header) and all reads and writes run beneath row-level security, so a source in another workspace is simply a 404. Errors use the shared { error, message } envelope, with an issues array added on body-validation failures.
/api/v1/connectorsscope data:read · capability connectors:readLists the workspace's connector sources, oldest first, credential-masked. No connection to the remote system is opened.
/api/v1/connectorsscope kb:write · capability connectors:writeRegisters a source and returns 201 with the credential-masked row. Creating a source does not start a crawl. The credential is JSON-serialized and encrypted at write; it is never returned by any read.
Request body
/api/v1/connectors/{id}scope data:read · capability connectors:readOne source plus its recent sync-run history, newest first (up to 50 runs). 404 when the id is unknown in this workspace.
/api/v1/connectors/{id}scope kb:write · capability connectors:writeEdits a source. At least one field is required (an empty body is a 400). A present credential is re-validated against the source's stored type and re-encrypted; omitting it keeps the stored secret. Returns the masked row.
Request body
/api/v1/connectors/{id}scope kb:write · capability connectors:writeRemoves the source and, by FK cascade, its sync_runs. Documents already ingested from it are retained and stay readable — their source_id simply dangles. Run a prune first if you want tombstones.
/api/v1/connectors/{id}/syncscope kb:write · capability connectors:executeEnqueues a crawl and returns 202. The body is optional — an empty body is parsed as an empty object. Enqueue only: the response says nothing about crawl progress, so poll GET /api/v1/connectors/{id} and read its syncRuns.
Request body
Two API-shape details worth pinning down
A paused source returns 409, coded invalid-config. The sync route maps the underlying connector error straight through, so the envelope is { "error": "invalid-config", "message": "Source <id> is paused" } at HTTP 409 — not a conflict code. Branch on the status, not the string.
Only full and incremental are reachable over HTTP. The data model and the worker both support prune (tombstone documents that vanished at the source) and permissions (re-read remote ACLs), and sync_runs.kind can hold all four — but the sync route's schema accepts only the first two, and SyncEnqueued.kind is typed to match. If you see a prune or permissions run in the history, it was enqueued server-side, not through this endpoint.
Data elements
Intake owns two tables and writes a defined set of columns on a third. All three are workspace-scoped under row-level security — every read and write goes through a workspace-bound transaction, so a cross-workspace id is invisible rather than forbidden.
| Table | One row is | Intake's relationship | RLS |
|---|---|---|---|
| connector_sources | One configured external source: type, config, encrypted credential, checkpoint, status. | Owns — created and updated only by this module. | Workspace-scoped |
| sync_runs | One append-only sync attempt, with per-run stats and a failure message. | Owns — written by the worker, read back through the detail endpoint. Cascades on source delete. | Workspace-scoped |
| documents | One knowledge document. Shared with the rest of the product. | Writes the provenance and ACL columns below, plus the ordinary content columns, on ingest. | Workspace-scoped |
| chunks | One retrievable slice of a document. | Written inline during ingest via the shared chunker — same path a manual save uses. | Workspace-scoped |
| audit_log | One recorded action. | Appends connector.create / connector.update / connector.delete / connector.ingest. Credential values are never written — an update records only the names of the changed fields. | Workspace-scoped |
Prune has a deliberate safety property
How it composes
Intake is the first slice in the capability map, so most of what it gains from the others is downstream of the documents it produces. Two of those links are real dependencies for a specific feature; the rest are additive.
- Atlas — entities and relations. Every ingested or changed document enqueues an extraction job. That job is what turns connector text into graph entities, and the enqueue is explicitly non-fatal: if nothing consumes it, ingestion still succeeds and the documents and chunks are all still there. Genuinely needs Atlas: anything that reads the entity graph over connector content.
- Lens — retrieval and grounded answers. Chunks are persisted inline during ingest, so lexical retrieval over connector documents works as soon as a sync completes. Embeddings are a separate queued job, so the vector half of hybrid retrieval only covers connector documents once that job has run against a configured embedding provider. Retrieval is also where the ACL columns are enforced: the content-ACL predicate filters on
acl_visibilityandacl_principalsin SQL before ranking, so arestrictedsource stays restricted end to end. - Lineage — provenance. Intake records a document revision on every connector upsert, tagged with the connector activity and a
sourceId:externalIdnote. The lineage read joinsconnector_sourcesto classify a document's origin as connector, upload, or vault. Without Lineage you still have the raw columns; with it you get the assembled trail. - Warrant — governance. Connector documents are ordinary
documentsrows, so document verification and review SLAs apply to them exactly as they do to hand-authored ones. Nothing in Intake requires it. - Crucible and Ports. Purely additive: evals measure quality over whatever corpus exists, and agents and MCP read it. Neither is consulted during a sync.
Intake connectors are not Conduit connectors
/api/v1/connectors (this module) and /api/v1/data/connectors (Conduit) are different surfaces backed by different tables — connector_sources versus data_connectors. This module ingests content into the corpus; Conduit queries live systems at answer time and stores nothing in documents. They share the data:read scope on their list endpoints and the same encryption envelope for credentials, and nothing else. Adopting Intake does not adopt Conduit.Deeper reading: Content connectors for per-source operational detail (rate limits, credential scopes), Connect Confluence, SharePoint & Slack for a walkthrough, and the OpenAPI reference for the wire contract.
Extending Intake
intakeConnectors opens the formerly closed Record<ConnectorType, factory>, so a contributed type resolves through the same buildConnector path, checkpoints, advisory lock and sync_runs rows as the four built-ins — or rewrite a document before it is persisted with intakeDocumentTransforms, which runs after normalisation and before hashing. Intake also emits document.ingested, document.updated, document.deleted, connector.source.* and connector.sync.completed / failed onto the plugin event bus. See the extension model for the contract and the trust boundary, and Build a plugin for a worked example.