Skip to documentation

Module references

Module reference / Bring it in

IntakeConnect & ingest

Intake

Read-only connectors and bulk import normalize source material into one deduplicated corpus.
  • Atlas

    Extract typed entities and relations with evidence.

  • Lineage

    Record source revisions and downstream provenance.

  • Lens

    Index source chunks for grounded retrieval.

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.

Intake connector execution

The HTTP surface only enqueues. The worker holds a per-source PostgreSQL advisory lock for the whole operation, so full, incremental, prune, and permissions work on one source can never interleave — across queues or replicas.

Rendering diagram

Downloads

Concepts

  • Intake flow
  • Modules
  • Intake

Keywords

  • keyed on source_id + external_id
  • enqueue only · 202
  • documents + chunks
  • append-only stats
  • Confluence · SharePoint · Slack
  • POST /connectors/:id/sync
  • worker · runConnectorSync
  • per-source advisory lock
  • fullLoad / poll / listIds
  • markdown + contentHash + aclPrincipals
  • checkpoint after every page
  • markdown_upload
  • sync_runs
  • connector_sources.sync_state
  • queue
  • connector
  • NormalizedDocument
Source and generation provenance

Status: current

Generated at: 2026-08-17T18:34:23.983Z

Source hash: 010a1cb78110aa28f45b2b818e38dfaec52d89133a13cd311002baf5fa69a79b

Metadata payload hash: e3e32783aa7f9b84b8fee71c7600042d808a364c724de4c5f14b4637fa263b93

Canonical appearance

src/app/(docs)/docs/modules/intake/page.tsx:51 route /docs/modules/intake

All appearances

  • canonicalsrc/app/(docs)/docs/modules/intake/page.tsx:51 route /docs/modules/intake

No mirrored appearances.

Generation versions

App: eli-ai 0.1.0

Mermaid: 11.16.0 · Mermaid CLI: 11.16.0

Node: v26.3.1 · Yarn: 4.17.1

Renderer config hash: 68c10966fe84406ee626034d58bfabd555df9f65f691204b7c46db24038da101

Renderer theme hash: c80287a78d80ad63d27bd5ca348b2ef9a7e2f44da289e436be6484ea28a1b033

Adapter versions: diagramGenerator=2, drawioFlowchart=1, drawioGantt=1, drawioSequence=1, drawioState=1

Full sidecar JSON: intake-connector-execution-010a1cb7.json

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.

  1. Give the key the right scopes

    Reads (GET /api/v1/connectors, GET /api/v1/connectors/{id}) require scope data:read. Everything that changes state — create, update, delete, and triggering a sync — requires scope kb: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:read for the reads, connectors:write for create/update/delete, and connectors:execute for the sync trigger. A missing capability is a 403 with error: "insufficient_capability".

  2. 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.

  3. Run a worker

    The sync endpoint returns 202 and stops there — it validates the source, checks it is not paused, and enqueues. If nothing is draining the queue, sources stay at lastSyncedAt: null forever and no sync_runs row is ever written. This is the one out-of-process dependency Intake genuinely has.

the whole module, headlessts
import { createEliTransport } from "@eli-ai/client/core";
import { createConnectorsClient } from "@eli-ai/client/connectors";

const connectors = createConnectorsClient(
  createEliTransport({
    baseUrl: "https://eli.ai",
    apiKey: () => process.env.ELI_API_KEY!,
  }),
);

// kb:write — register a source. This does NOT start a crawl.
const source = await connectors.create({
  type: "confluence",
  config: { baseUrl: "https://acme.atlassian.net/wiki", spaceKeys: ["ENG"] },
  credential: { email: "svc@acme.com", apiToken: process.env.CONFLUENCE_TOKEN! },
});

// kb:write — enqueue the crawl. Resolves as soon as the job is queued.
await connectors.sync(source.id, { kind: "full" });

// data:read — poll the source and its sync-run history.
const detail = await connectors.get(source.id);
console.log(detail.status, detail.lastSyncedAt, detail.syncRuns[0]?.stats);

What Intake does NOT require

No ontology or entity extraction, no model provider or embedding key, no agent definitions, no eval config, no reranker, and no query surface. A source syncs, documents and chunks land, and the credential is encrypted at rest — all of that works with every other module absent. Two capabilities are genuinely deferred to queued follow-on jobs rather than done inline: entity extraction and embeddings — see how it composes below.

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

labelsPartial<EliConnectorsLabels>Per-string overrides merged over the defaults. Every visible string except the two state labels comes from here.
canSyncbooleanRenders the per-card sync kind picker and Sync button. Defaults to true. Set false for a read-only view.
onConfigure(connector?: ConnectorSource) => voidSupplies your own create/edit UI. Passing it adds a header button (called with no argument) and a per-card button (called with that source). Omitting it renders no configure affordance at all.
onConnectorSelect(connector: ConnectorSource) => voidFired when the card's name button is activated. Use it to open a detail view.
onSyncQueued(result: SyncEnqueued) => voidFired after the sync mutation resolves, with the 202 body. The list is refetched first.
titlestringOverrides the surface heading. Falls back to labels.title.
descriptionstringOverrides the sub-heading. Falls back to labels.description.
loadingLabelstringText for the loading state. Defaults to "Loading content connectors…".
errorLabelstringText for the list-level error state (which also renders a retry button). Defaults to "Content connectors could not be loaded.".
onError(error: unknown) => voidCalled for both the list query and the sync mutation.
classNamestringAppended after the built-in "eli-root" class on the root section.

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)

titlestringDefault: "Content connectors".
descriptionstringDefault: "Monitor governed content intake and start safe, server-side synchronizations.".
configurestringDefault: "Configure connector".
syncstringDefault: "Sync". Shown on the idle sync button.
fullSyncstringDefault: "Full sync". The kind-picker option.
incrementalSyncstringDefault: "Incremental sync". The kind-picker option, and the default selection.
emptystringDefault: "No content connectors are configured.".
credentialReadystringDefault: "Credential stored".
credentialMissingstringDefault: "Credential required".
lastSyncedstringDefault: "Last synced". Prefixes the formatted lastSyncedAt.
provider wiring + EliConnectorstsx
import { createEliTransport } from "@eli-ai/client/core";
import { EliProvider } from "@eli-ai/react/provider";
import { EliConnectors } from "@eli-ai/react/connectors";
import type { ConnectorSource } from "@eli-ai/contracts/connectors";
import "@eli-ai/react/styles.css";

// The transport is the trust boundary: point it at a same-origin route that
// injects the Bearer key server-side. Never ship an eli_sk_ key to a browser.
const transport = createEliTransport({ fetch: sameOriginProxyFetch });

export function IntakePanel({ workspaceId }: { workspaceId: string }) {
  return (
    <EliProvider transport={transport} workspaceId={workspaceId}>
      <EliConnectors
        labels={{ title: "Sources", sync: "Run now" }}
        onConfigure={(connector?: ConnectorSource) => openSourceDialog(connector)}
        onSyncQueued={(result) => toast(`Queued a ${result.kind} sync`)}
        onError={(error) => reportError(error)}
      />
    </EliProvider>
  );
}

What this component deliberately does not do

It never creates, edits, or deletes a source, and it never renders sync-run history. It issues exactly two calls: 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).

ConnectorsClient surfacets
import { createConnectorsClient } from "@eli-ai/client/connectors";

const client = createConnectorsClient(transport);

client.list();                            // → ConnectorSourceList
client.get(id);                           // → ConnectorSourceDetail (source + syncRuns)
client.create(input);                     // → ConnectorSource
client.update(id, input);                 // → ConnectorSource
client.remove(id);                        // → DeleteConnectorResult ({ deleted, id })
client.sync(id, { kind: "incremental" }); // → SyncEnqueued

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.

GET/api/v1/connectorsscope data:read · capability connectors:read

Lists the workspace's connector sources, oldest first, credential-masked. No connection to the remote system is opened.

Example requestbash
curl https://eli.ai/api/v1/connectors \
  -H "Authorization: Bearer $ELI_API_KEY"
Responsejson
{
  "items": [
    {
      "id": "01JZQ3W8N4B7K2XR9V0C5T6HMD",
      "workspaceId": "01JH8Z2P0KQ4RN6TS3WVYA1BCE",
      "type": "confluence",
      "config": { "baseUrl": "https://acme.atlassian.net/wiki", "spaceKeys": ["ENG"] },
      "syncState": {},
      "status": "active",
      "aclVisibility": "workspace",
      "hasCredential": true,
      "lastSyncedAt": "2026-07-30T11:04:02.118Z",
      "createdBy": "01JH8Z2P0KQ4RN6TS3WVYA1BCE",
      "createdAt": "2026-07-01T09:00:00.000Z",
      "updatedAt": "2026-07-30T11:04:02.118Z"
    }
  ],
  "total": 1
}
POST/api/v1/connectorsscope kb:write · capability connectors:write

Registers 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

typerequired"confluence" | "sharepoint" | "slack" | "markdown_upload"Discriminates the body. Immutable afterwards — PATCH cannot change it.
credentialobjectStructured per type: confluence { apiToken, email? }, sharepoint { tenantId, clientId, clientSecret }, slack { botToken }. Required for those three; markdown_upload accepts no credential key at all.
configobjectNon-secret source config. Validated per type: confluence requires a valid http(s) baseUrl (optional spaceKeys array); sharepoint requires a non-empty siteIds string array; slack and markdown_upload require nothing. Defaults to {}.
status"active" | "paused" | "error"Defaults to "active". A paused source rejects sync with 409.
aclVisibility"workspace" | "restricted"Defaults to "workspace". "restricted" forces every document this source ingests to be restricted.
Example requestbash
curl -L -X POST https://eli.ai/api/v1/connectors \
  -H "Authorization: Bearer $ELI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "confluence",
    "config": { "baseUrl": "https://acme.atlassian.net/wiki", "spaceKeys": ["ENG"] },
    "credential": { "email": "svc@acme.com", "apiToken": "<atlassian-api-token>" },
    "aclVisibility": "workspace"
  }'
Responsejson
{
  "id": "01JZQ3W8N4B7K2XR9V0C5T6HMD",
  "workspaceId": "01JH8Z2P0KQ4RN6TS3WVYA1BCE",
  "type": "confluence",
  "config": { "baseUrl": "https://acme.atlassian.net/wiki", "spaceKeys": ["ENG"] },
  "syncState": {},
  "status": "active",
  "aclVisibility": "workspace",
  "hasCredential": true,
  "lastSyncedAt": null,
  "createdBy": "01JH8Z2P0KQ4RN6TS3WVYA1BCE",
  "createdAt": "2026-07-01T09:00:00.000Z",
  "updatedAt": "2026-07-01T09:00:00.000Z"
}
GET/api/v1/connectors/{id}scope data:read · capability connectors:read

One source plus its recent sync-run history, newest first (up to 50 runs). 404 when the id is unknown in this workspace.

Example requestbash
curl https://eli.ai/api/v1/connectors/01JZQ3W8N4B7K2XR9V0C5T6HMD \
  -H "Authorization: Bearer $ELI_API_KEY"
Responsejson
{
  "id": "01JZQ3W8N4B7K2XR9V0C5T6HMD",
  "workspaceId": "01JH8Z2P0KQ4RN6TS3WVYA1BCE",
  "type": "confluence",
  "config": { "baseUrl": "https://acme.atlassian.net/wiki", "spaceKeys": ["ENG"] },
  "syncState": {},
  "status": "active",
  "aclVisibility": "workspace",
  "hasCredential": true,
  "lastSyncedAt": "2026-07-30T11:04:02.118Z",
  "createdBy": "01JH8Z2P0KQ4RN6TS3WVYA1BCE",
  "createdAt": "2026-07-01T09:00:00.000Z",
  "updatedAt": "2026-07-30T11:04:02.118Z",
  "syncRuns": [
    {
      "id": "01JZR0M6C9F3H8QT4Y7XZB2NAV",
      "workspaceId": "01JH8Z2P0KQ4RN6TS3WVYA1BCE",
      "sourceId": "01JZQ3W8N4B7K2XR9V0C5T6HMD",
      "kind": "incremental",
      "startedAt": "2026-07-30T11:03:51.002Z",
      "finishedAt": "2026-07-30T11:04:02.118Z",
      "stats": {
        "docsSeen": 128,
        "docsAdded": 4,
        "docsUpdated": 9,
        "docsTombstoned": 0,
        "pages": 3
      },
      "error": null
    }
  ]
}
PATCH/api/v1/connectors/{id}scope kb:write · capability connectors:write

Edits 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

configobjectReplaces the stored config wholesale, then re-validated against the source's type.
credentialobjectAny of the three structured shapes. Because type is immutable the body cannot discriminate, so a shape that does not match the stored type is rejected with 400.
status"active" | "paused" | "error"Pause or resume the source.
aclVisibility"workspace" | "restricted"Change the default ACL policy.
Example requestbash
curl -L -X PATCH https://eli.ai/api/v1/connectors/01JZQ3W8N4B7K2XR9V0C5T6HMD \
  -H "Authorization: Bearer $ELI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "paused" }'
Responsejson
{
  "id": "01JZQ3W8N4B7K2XR9V0C5T6HMD",
  "workspaceId": "01JH8Z2P0KQ4RN6TS3WVYA1BCE",
  "type": "confluence",
  "config": { "baseUrl": "https://acme.atlassian.net/wiki", "spaceKeys": ["ENG"] },
  "syncState": {},
  "status": "paused",
  "aclVisibility": "workspace",
  "hasCredential": true,
  "lastSyncedAt": "2026-07-30T11:04:02.118Z",
  "createdBy": "01JH8Z2P0KQ4RN6TS3WVYA1BCE",
  "createdAt": "2026-07-01T09:00:00.000Z",
  "updatedAt": "2026-07-31T08:12:44.500Z"
}
DELETE/api/v1/connectors/{id}scope kb:write · capability connectors:write

Removes 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.

Example requestbash
curl -L -X DELETE https://eli.ai/api/v1/connectors/01JZQ3W8N4B7K2XR9V0C5T6HMD \
  -H "Authorization: Bearer $ELI_API_KEY"
Responsejson
{ "deleted": true, "id": "01JZQ3W8N4B7K2XR9V0C5T6HMD" }
POST/api/v1/connectors/{id}/syncscope kb:write · capability connectors:execute

Enqueues 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

kind"full" | "incremental"Defaults to "incremental" — a delta poll windowed from the source's lastSyncedAt to now. "full" is a complete re-crawl, resumed from the stored sync_state checkpoint.
Example requestbash
curl -L -X POST https://eli.ai/api/v1/connectors/01JZQ3W8N4B7K2XR9V0C5T6HMD/sync \
  -H "Authorization: Bearer $ELI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "kind": "full" }'
Responsejson
{
  "enqueued": true,
  "sourceId": "01JZQ3W8N4B7K2XR9V0C5T6HMD",
  "kind": "full"
}

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.

TableOne row isIntake's relationshipRLS
connector_sourcesOne configured external source: type, config, encrypted credential, checkpoint, status.Owns — created and updated only by this module.Workspace-scoped
sync_runsOne 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
documentsOne 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
chunksOne retrievable slice of a document.Written inline during ingest via the shared chunker — same path a manual save uses.Workspace-scoped
audit_logOne 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
connector_sources — the columns a consumer reads
ColumnTypeWhat it means to a consumer
idtext (ULID)The id used in every /connectors/{id} path.
typeenumconfluence | sharepoint | slack | markdown_upload. Set at create, immutable after.
configjsonbNon-secret settings, shape per type (baseUrl/spaceKeys, siteIds/driveIds, channelIds/teamId/oldest, documents). Returned verbatim.
credential_enctext · never returnedAES-256-GCM ciphertext. Stripped from every API read and replaced with the boolean hasCredential. Decrypted only in the worker at crawl time.
sync_statejsonbThe resumable checkpoint (delta link / CQL watermark / per-channel cursors), rewritten after every crawled page. Opaque — treat it as a black box.
statusenumactive | paused | error. A failed sync sets error; a successful one sets it back to active. paused blocks the sync endpoint (409).
acl_visibilityenumworkspace | restricted. "restricted" forces every document this source ingests to be restricted, and a content update can never downgrade it.
last_synced_attimestamptz | nullSet on a successful sync. Also the lower bound of the next incremental poll window; null means poll from the epoch.
created_bytext | nullThe actor id, or the key id when there is no actor.
sync_runs — append-only history
ColumnTypeWhat it means to a consumer
source_idtext → connector_sources.idFK, cascade delete.
kindenumfull | incremental | prune | permissions. Only the first two can be enqueued over HTTP.
started_at / finished_attimestamptzfinished_at is null while the run is in flight — that is how you detect an active sync.
statsjsonb{ docsSeen, docsAdded, docsUpdated, docsTombstoned, pages }. Written once when the run finishes, on success and on failure alike.
errortext | nullThe failure message when the run threw; null on success. The job is retried by the queue, producing another row.
documents — the columns Intake sets
ColumnTypeWhat it means to a consumer
source_idtext | nullWhich connector source produced this document. Null means a manual or vault document — that is the pre-Intake default and stays untouched.
external_idtext | nullThe document's id at the source. (workspace_id, source_id, external_id) is uniquely indexed where source_id is not null, which is what makes re-sync an upsert rather than a duplicate.
source_versiontext | nullThe source's own version token (Confluence version number, Graph eTag, Slack ts), falling back to the content hash. Unchanged and live ⇒ the document is skipped entirely, so no re-chunk and no re-embed.
acl_visibilitytextworkspace | restricted, defaulting to workspace. Set from the source policy and any permissions refresh; a later content sync never downgrades a restricted document.
acl_principalsjsonb (string[])Normalized principal ids carried over from the source. Retrieval filters on this pair in SQL before ranking.
pathtextDeterministic vault path — connectors/<source id>/<sanitized external id>-<8 hex>.md — so the markdown stays readable through the ordinary document surfaces.
content_hash / versiontext / integerHash of the stored markdown; version increments on each changed re-sync.
deleted_attimestamptz | nullSet by a prune when the document vanished at the source. A later sync that sees it again clears it, reactivating the same row rather than creating a new one.

Prune has a deliberate safety property

A prune builds the complete set of currently-present external ids before deleting anything, so a source outage mid-enumeration aborts the run instead of mass-tombstoning the corpus. Tombstoning more than half of a source's live documents is logged as a warning but still executed.

Feature map

Behind the six operations and the single component, the runtime divides into five feature areas. Each area names the concrete modules and tables that deliver it, and the same names reappear in the code map and the internals section below, so every behavioural claim on this page can be traced to the code that enforces it.

Feature areaWhat it providesDelivered bySurfaces
Source registry and credential custodyTyped source records with per-type config validation; credentials serialized and encrypted at write, stripped from every read.createSource / updateSource / maskSource in src/server/connectors/crud.ts; the connector_sources tableREST CRUD under /api/v1/connectors · EliConnectors cards · connectors SDK client
Sync execution and checkpointingEnqueue-only trigger, queue-dispatched worker runners, one advisory lock per source, and a resumable checkpoint persisted after every crawled page.triggerSync in crud.ts; runConnectorSync and withConnectorSourceLock in src/server/connectors/sync.ts; the queue in src/server/jobs/index.tsPOST /api/v1/connectors/{id}/sync (202) · background worker process
Normalization and deduplicated persistencePer-type source adapters, binary and HTML normalization to markdown, deterministic chunking, and an upsert keyed on (source_id, external_id) that skips unchanged revisions.Adapters in src/server/connectors/sources; src/server/connectors/normalize.ts; chunkMarkdown + persistChunks in src/server/ingest/chunking.tsdocuments and chunks rows served by the document catalog and retrieval
ACL mirroring and prune lifecycleSource-level ACL policy, per-document principals mirrored from the remote system, a permissions runner that re-reads remote ACLs, and outage-safe tombstoning.runConnectorPermissions / runConnectorPrune in sync.ts; normalizeContentPrincipals in src/server/authz/content-acl.tsacl_visibility / acl_principals columns, filtered in SQL by every retrieval read
Run observability and enrichment handoffAppend-only per-run stats, source status transitions, audit events, and non-fatal handoff of extraction and embedding work to queued jobs.startSyncRun / finishSyncRun in sync.ts; the sync_runs and audit_log tables; enqueueExtract / enqueueEmbed in jobssyncRuns on GET /api/v1/connectors/{id} · status badges in EliConnectors

System architecture

The topology below names the real components: route directories, exported server functions, the job queue, and the tables each stage writes. Rectangles are API routes and services, cylinders are tables and queues, and stadium nodes are the external systems a crawl reads. Intake calls no AI model anywhere in this path — embeddings and entity extraction are downstream jobs owned by other modules.

Intake — system architecture

The HTTP surface writes connector_sources and enqueues; everything else happens in the worker, which holds the per-source advisory lock while adapters crawl, normalization produces markdown, and the keyed upsert lands documents, chunks, revisions, and audit rows.

Rendering diagram

Downloads

Concepts

  • Intake architecture
  • Sync execution
  • Normalization pipeline
  • External systems
  • REST surface — src/app/api/v1/connectors
  • Modules
  • Intake

Keywords

  • Connector CRUD routes (API)
  • Sync trigger route (API)
  • store: queue
  • normalize.ts — binary and HTML to markdown (service)
  • store: table
  • enqueue extract + embed
  • Worker dispatcher — (service)
  • chunkMarkdown + persistChunks (service)
  • SharePoint / Microsoft Graph
  • withConnectorSourceLock — per-source advisory lock (service)
  • persistConnectorDocument — keyed upsert (service)
  • src/worker/index.ts
  • runConnectorSync / runConnectorPrune / runConnectorPermissions (service)
  • Confluence
  • Slack
Source and generation provenance

Status: current

Generated at: 2026-08-17T18:34:28.173Z

Source hash: 08a0d58c95196f25a99720e1ef6c0c6e697d179154b06415942faa7a1332ca6a

Metadata payload hash: 70781767ea9e8b27d99f3b0cc832bcfc8411a9f1e1f3997405255eca404ee1b0

Canonical appearance

src/app/(docs)/docs/modules/intake/page.tsx:68 route /docs/modules/intake

All appearances

  • canonicalsrc/app/(docs)/docs/modules/intake/page.tsx:68 route /docs/modules/intake

No mirrored appearances.

Generation versions

App: eli-ai 0.1.0

Mermaid: 11.16.0 · Mermaid CLI: 11.16.0

Node: v26.3.1 · Yarn: 4.17.1

Renderer config hash: 68c10966fe84406ee626034d58bfabd555df9f65f691204b7c46db24038da101

Renderer theme hash: c80287a78d80ad63d27bd5ca348b2ef9a7e2f44da289e436be6484ea28a1b033

Adapter versions: diagramGenerator=2, drawioFlowchart=1, drawioGantt=1, drawioSequence=1, drawioState=1

Full sidecar JSON: intake-system-architecture-08a0d58c.json

How to read the Intake system architecture

  1. Start at the two route shapes: CRUD routes touch only connector_sources; the sync route touches only the job queue. No HTTP request ever performs a crawl.
  2. Follow the worker lane: The worker dispatcher claims queued jobs and hands them to the runner functions, which acquire the per-source advisory lock before any remote I/O.
  3. Trace one document through the pipeline: An adapter emits a normalized document, normalize.ts flattens binaries and HTML to markdown, and persistConnectorDocument upserts it with its chunks, revision, and audit row.
  4. Note the dotted handoff: After a commit, the upsert enqueues extraction and embedding jobs back onto the same queue — non-fatal, so ingestion succeeds even with no consumer.
Trust boundary
Credentials decrypt only inside the worker runner, remote fetches run through an origin-allowlisted client, and every table write happens inside a workspace-scoped transaction under row-level security.
Durable state
connector_sources checkpoints, append-only sync_runs, deduplicated documents with provenance and ACL columns, chunks, document_revisions, and audit_log rows.

Failure paths

  • No worker draining the queue leaves sources at lastSyncedAt null forever
  • A remote outage mid-crawl fails the run after its checkpoint, so the retry resumes
  • A paused source rejects the sync trigger with 409
  • An unreadable credential fails the run before any remote call

Signals

  • sync_runs stats per run: docsSeen, docsAdded, docsUpdated, docsTombstoned, pages
  • connector_sources.status flipping to error after a failed run
  • lastSyncedAt staleness per source
  • audit_log connector.* event volume

Where the logic lives

The code map below is the module boundary in file terms. Every path is real; table names live in the notes because tables are defined in the schema module, not in files of their own.

ComponentKindLives atNotes
Connector CRUD routesAPIsrc/app/api/v1/connectorsList + create at the collection; get, patch, delete under the id segment. Reads guard on scope data:read, writes on kb:write.
Sync trigger routeAPIsrc/app/api/v1/connectors/[id]/syncEnqueue-only 202; capability connectors:execute; a paused source is a 409.
Source CRUD and credential envelopeservicesrc/server/connectors/crud.tsAES-256-GCM encryption via src/lib/crypto.ts; every exported read returns hasCredential, never ciphertext.
Sync runners and advisory lockservicesrc/server/connectors/sync.tsrunConnectorSync / runConnectorPrune / runConnectorPermissions plus persistConnectorDocument, the keyed upsert.
Source adaptersservicesrc/server/connectors/sourcesfullLoad / poll / listIds / getPermissions per type; built by src/server/connectors/registry.ts.
Content normalizationservicesrc/server/connectors/normalize.tsAvailability-gated extractors for PDF, DOCX, Office formats, and HTML; a missing extractor degrades to a skip, never a crash.
Deterministic chunkerservicesrc/server/ingest/chunking.tsHeading-aware splitting toward 1000 tokens, hard cap 1600; persistChunks reconciles rows by identity so valid chunk_embeddings survive.
Job queueservicesrc/server/jobs/index.tspg-boss enqueue helpers for full / incremental / prune / permissions syncs plus extract, embed, and cache-invalidate jobs.
Worker dispatcherservicesrc/worker/index.tsThe out-of-process consumer that dispatches queued jobs to the runner functions by name.
Ingestion tablesstoresrc/server/db/schema.tsconnector_sources, sync_runs, documents, chunks, document_revisions, audit_log — each with a tenant-isolation RLS policy.
Content-ACL predicateservicesrc/server/authz/content-acl.tsNormalizes principals on write and builds the SQL predicate that filters acl_visibility / acl_principals on read.
Guarded outbound fetchservicesrc/server/network/safe-fetch.tsOrigin-allowlisted, size- and time-bounded HTTP client handed to every connector.
EliConnectors surfaceUIpackages/react/src/connectors/index.tsxThe card grid documented above; issues exactly list() and sync().
Connectors clientSDKpackages/client/src/connectors.tscreateConnectorsClient — list, get, create, update, remove, sync over the shared transport.
Wire contractsSDKpackages/contracts/src/connectors.tsConnectorSource, ConnectorSourceDetail, SyncEnqueued, and the sync-kind unions.

Primary runtime flow

The highest-value flow is a triggered sync: one HTTP call, then everything else in the worker. Message labels use the operation names from the HTTP API section above.

Intake — primary runtime flow

syncConnector returns 202 immediately; the worker acquires the source lock, decrypts the credential, crawls page by page with a checkpoint after each, and finishes by stamping the source and the run row. getConnector is how a caller observes progress.

Rendering diagram

Downloads

Concepts

  • Intake runtime flow
  • Modules
  • Intake

Keywords

  • routes (API)
  • API consumer (SDK or curl)
  • pg-boss job queue (store)
  • sync_runs (store)
  • connector_sources (store)
  • persist the page checkpoint into sync_state
  • upsert on (source_id, external_id), skip unchanged sourceVersion
  • runConnectorSync (service)
  • Remote source (external)
  • documents + chunks (store)
  • syncConnector with kind full or incremental
  • triggerSync validates the source and enqueues
  • worker dispatches the sync job
  • acquire the per-source advisory lock
  • startSyncRun appends an in-flight run row
  • fullLoad or poll fetches one page
  • normalized documents plus checkpoint
  • mark status active and stamp lastSyncedAt
  • finishSyncRun writes final stats
  • getConnector reads the source and its syncRuns
  • loadSourceForSync decrypts the stored credential
  • /api/v1/connectors
Source and generation provenance

Status: current

Generated at: 2026-08-17T18:34:26.094Z

Source hash: c82b4a3473a2610cc04b9ba32436ae16062c524bbb1fbb1d01daf347b3711083

Metadata payload hash: 1a00847df07b591596c12d491477705ce29f32fd025249fd23091efcae1d0e4a

Canonical appearance

src/app/(docs)/docs/modules/intake/page.tsx:123 route /docs/modules/intake

All appearances

  • canonicalsrc/app/(docs)/docs/modules/intake/page.tsx:123 route /docs/modules/intake

No mirrored appearances.

Generation versions

App: eli-ai 0.1.0

Mermaid: 11.16.0 · Mermaid CLI: 11.16.0

Node: v26.3.1 · Yarn: 4.17.1

Renderer config hash: 68c10966fe84406ee626034d58bfabd555df9f65f691204b7c46db24038da101

Renderer theme hash: c80287a78d80ad63d27bd5ca348b2ef9a7e2f44da289e436be6484ea28a1b033

Adapter versions: diagramGenerator=2, drawioFlowchart=1, drawioGantt=1, drawioSequence=1, drawioState=1

Full sidecar JSON: intake-primary-runtime-flow-c82b4a34.json

How to read the Intake primary runtime flow

  1. Watch the 202 boundary: The route validates the source, enqueues, and answers — crawl progress is never part of the HTTP response.
  2. Note where the credential appears: loadSourceForSync decrypts inside the worker runner, after the advisory lock is held; no route handler ever sees plaintext.
  3. Follow the page loop: Each page upserts its documents and then persists the checkpoint, so a crash between pages resumes from the last completed page.
  4. Check the terminal writes: Success stamps lastSyncedAt and resets status to active; failure records the error on the run row and flips the source to error before the job retries.
Trust boundary
Scope kb:write plus capability connectors:execute gate the trigger; the crawl itself runs with the source's own credential against an origin-allowlisted fetch.
Durable state
One sync_runs row per attempt, the updated sync_state checkpoint, and every upserted document with its chunks, revision, and audit row.

Failure paths

  • Queue with no consumer: the 202 is honest but nothing ever runs
  • Remote rate limiting or outage fails the run; the queue retries with the same checkpoint
  • sourceVersion collisions never occur — unchanged documents are skipped, not rewritten
  • Advisory-lock contention serializes overlapping triggers instead of interleaving them

Signals

  • finished_at null on a sync_runs row marks an in-flight run
  • pages and docsSeen growing across runs of the same source
  • error text on failed runs
  • status transitions on connector_sources

Internals and invariants

Source registry and credential custody

A source is created and edited through src/server/connectors/crud.ts, which validates non-secret config per type (a bad Confluence base URL or an empty SharePoint site list is a 400 at write time, not a failed crawl later), validates the credential shape against the stored type, and serializes the credential into an encrypted column. Reads go through a masking projection that replaces the ciphertext with a boolean.

  • Invariant — Credential ciphertext never leaves the CRUD module unmasked: every exported read strips it to hasCredential, and decryption happens only in loadSourceForSync at crawl time, enforced in src/server/connectors/crud.ts.
  • Invariant — Audit rows for source updates record only the names of the changed fields, never credential or config values, enforced in src/server/connectors/crud.ts.

Sync execution and checkpointing

The trigger validates the source and enqueues; the worker claims the job and hands it to a runner. Every runner first takes a session-level PostgreSQL advisory lock keyed on the source, then crawls with the adapter, persisting the generator's checkpoint after each page. Remote I/O happens outside any database transaction — only the short bookkeeping writes open one.

  • Invariant — All four run kinds against one source serialize under a single advisory lock that spans queues and worker replicas, enforced by withConnectorSourceLock in src/server/connectors/sync.ts.
  • Invariant — The resumable checkpoint is persisted after every crawled page, so a crash resumes rather than restarts, enforced by the crawl driver in src/server/connectors/sync.ts.

Normalization and deduplicated persistence

Adapters emit normalized documents; binary payloads flatten to markdown through availability-gated extractors. Persistence is a single upsert: frontmatter is injected, the document id is spliced in, content is hashed and chunked, and the row lands with its revision and audit entry in one workspace transaction. An existing live row with an unchanged source version is skipped before any of that work happens.

  • Invariant — A live document whose sourceVersion matches is skipped outright — no rewrite, no re-chunk, no re-embed — enforced by persistConnectorDocument in src/server/connectors/sync.ts.
  • Invariant — Chunk rows reconcile by identity (document, normalized content hash, position), so existing chunk embeddings survive exactly when they are still valid, enforced by persistChunks in src/server/ingest/chunking.ts.
  • Invariant — A plugin document transform may rewrite or deliberately skip a document, but a transform that throws or returns garbage is ignored and the last good version persists; identity keys are pinned by the host, enforced in src/server/connectors/sync.ts.

ACL mirroring and prune lifecycle

Documents carry the visibility and principals their source reported. The permissions runner re-reads remote ACLs for every live document of a source; the prune runner enumerates the complete set of ids still present remotely and tombstones only what has verifiably vanished.

  • Invariant — A content update never downgrades a restricted document, and a restricted source forces every document it ingests to restricted, enforced in the ACL merge of src/server/connectors/sync.ts.
  • Invariant — A prune builds the complete present-id set before deleting anything; an enumeration failure aborts the run with zero tombstones, enforced by runConnectorPrune in src/server/connectors/sync.ts.

Run observability and enrichment handoff

Every attempt writes a run row at start and finalizes it with stats and an error string on completion — success and failure alike — while the source row tracks status and last-synced time. After a successful upsert, extraction and embedding jobs are enqueued for downstream modules and a cache invalidation is signalled for changed content.

  • Invariant — Every sync attempt leaves an append-only sync_runs row with its stats, written by startSyncRun / finishSyncRun in src/server/connectors/sync.ts.
  • Invariant — Extraction and embedding handoffs are non-fatal: an enqueue failure logs a warning and never fails ingestion, enforced in src/server/connectors/sync.ts.

For a self-hosting team the safe extension points are exactly the seams above: contribute a new source type through intakeConnectors (it resolves through the same buildConnector path in src/server/connectors/registry.ts and inherits the lock, checkpoints, and run rows), rewrite documents pre-persist with intakeDocumentTransforms, subscribe to the ingestion events on the plugin bus under src/server/plugins, or swap the vault body store in src/server/vault/store between its filesystem and Postgres implementations. The upsert key, the advisory lock, and the credential masking are load-bearing and not configurable.

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_visibility and acl_principals in SQL before ranking, so a restricted source stays restricted end to end.
  • Lineage — provenance. Intake records a document revision on every connector upsert, tagged with the connector activity and a sourceId:externalId note. The lineage read joins connector_sources to 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 documents rows, 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

A plugin can add a source typeintakeConnectors 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.