← Harvester

Sources

harvester · workzone

What a data source is and how it connects. A connection is a source model filled in by the admin: type, address, credentials, and scope. From there the platform authenticates, tucks secrets away in the crypto core, verifies the connection via Test Connection, and begins fetching only the objects in scope. From there the source runs through its own lifecycle — pause, disconnect, delete. The connection screen lives in Admin Panel.

Source model Filled in by the admin at connection time.
connector_type The source type — Jira, Confluence, GitLab, Slack, and others. Selects a connector from the registry; its manifest defines which fields and what kind of credentials the form asks for.
base URL The address of the source instance — where the connector sends its requests.
credential How the platform presents itself to the source: as whom (a service account or a personal account) and with what (a token, or OAuth in v2). Depends on connector_type. Details below.
scope Which source objects to fetch — projects, spaces, channels. A mode plus lists set by the admin. See scope.
Authentication

Signing in to a source breaks down into two independent questions: as whom the platform accesses the source and with what it proves that. The axes are orthogonal and combine — a service account presents its own token.

As whom — the identity
Service account
A dedicated read-only technical account for the integration. Its permissions are visible and revocable in one place, its scope trimmed to what was selected. The default path.
Admin's personal account
A fallback for instances where a service account can't be set up or would be overkill. Access is tied to a person — fine for small, one-off sources, not as the norm.
With what — the mechanism
Static token
An API token or personal access token issued for the chosen identity. Long-lived, entered once — the platform tucks it away in the crypto core.
OAuth v2
Authorization through the source's provider, without manually entering a long-lived token.

Service-account tokens and passwords are stored encrypted — the platform needs the original back (the connector uses it to reach the source), so this is encryption, not a hash. The key and algorithm aren't home-grown: the same primitive (AES-256-GCM) that encrypts the SMTP password in the Email module is provided by the Auth crypto core. Harvester keeps no secret store of its own — it writes the secret to the sources.credential_enc field and calls the core to decrypt it at the moment it queries the source.

Test Connection

Before the first fetch, the connection is checked step by step — so that on failure it's immediately clear where the problem is: the address, the credentials, or the permissions. This is the same availability-check approach used by model providers and Email (is_available()): not "works / doesn't work," but a clear diagnosis. The "credentials" and "permissions" steps are the connector's check_connection() method; the URL-availability step is a shared layer, the same for every type, not the connector's job.

1
URL reachable
The instance at base URL responds. If not — the address is wrong or the source is unreachable over the network.
2
Credentials valid
The source accepts the credential. If not — the token has expired, been revoked, or the service account is the wrong one.
3
Permissions sufficient
The account can actually see the selected objects. If not — the credentials are valid, but read access to the project or channel is missing.
Each step returns its own error: the admin fixes exactly the link that broke, rather than guessing from a generic "couldn't connect."

The same probe runs on two triggers.
On demand — the admin hits it at connection and edit time, with all three steps.
On a schedule — the platform runs a lightweight version itself (steps 1–2: connectivity and credential validity) between syncs, separate from the heavy run. On a fetch failure: the probe drops the source into a health state of (error) and raises a notification through the same channel as a run failure (visibility). The triggers and the probe's outcome show up in the "Test Connection" block of the source card.

Scope

Scope is a mode, not a list typed in by hand. As soon as the credentials are accepted (Test Connection, step 2), the connector builds a catalog of the source's objects — projects, spaces, channels — via its list_catalog() method, and the admin picks from a live list on the connection screen. No typos, no guessing at keys.

Scope mode
Everything
Take the source whole and pick up new objects automatically; a deny-list strips out the excess — archives, sandboxes, personal spaces. For large instances, where listing every object is overkill. The default path.
Selected only
A named allow-list from the catalog, with search. New objects stay outside until the admin adds them. For pilots and targeted connections.

Scope is stored as a policy — a mode plus lists, not a frozen snapshot: the platform reconciles the specific objects against the catalog on every sync, or else newly created and deleted spaces would drift from the settings. A note on permissions: to build the catalog, the account needs to list the instance, even when the fetch is later narrowed. Scope is part of the source model and the boundary within which the fetch operates.

Fetch mechanics

How the connector pulls data from the source and turns a single loop: the listing method returns a page, the connector feeds it into the pipeline and goes for the next one by cursor. A single failed request — a timeout or a 429 from the source — doesn't break the loop: retry with backoff keeps reliability.

listing The source's API method that returns objects page by page.
page The next page of objects.
emit The page's objects stream into the pipeline.
next cursor exists → back to listing; none → fetch complete
Pagination · cursor The source returns data in pages; the cursor is a bookmark to the next one. The connector pages to the end, rather than trying to grab everything in one request.
Incremental · fetch(since) The "fetch only what changed after" boundary. On a full import since = epoch; on an incremental it's the moment of the previous sync.
Webhooks Near-real-time: the source itself sends an event about a change — this is a trigger, not new logic (the path is below). One channel per source; the admin sets up the endpoint and secret manually in the source settings (auto-provisioning via the source's API is v2).
Webhook event path
event The source sends a call to the endpoint: "an object changed."
inbound check Signature and anti-replay — security rejects anything foreign.
incremental task The event becomes a "pull the delta" task.
pipeline The same pipeline as a scheduled run.

Concurrency is between sources: their runs are independent and go in parallel (per-source). Within a single source the listing is sequential — the cursor keeps it from running ahead — but object processing is order-independent.

The same source APIs also return users with their access rights — but that's a separate topic: how identity and ACL move into the platform is owned by acl-identity.

Source lifecycle

A connected source outlives any single run: it gets paused, disconnected, deleted. Behind the screen's buttons stand states and rules. Authentication splits into "as whom" and "with what."

State — what the admin decided
Active
Connected and syncing on schedule. The working state.
Paused
The schedule is paused while the source is down. Configuration and collected data are intact, no new runs start; lifted with Resume.
Disconnected
Credentials removed, runs stopped, but the config shell and data are preserved. The source is reconnected without setting it up again — for access rotation or a temporary standdown.
Health — what the platform observes
idle
No runs; the source is waiting for the next scheduled one.
syncing
A run is in progress; its course, progress, and outcome are held by SyncRun.
error
The last run or health check ended in failure. It doesn't block configuration — it's fixed by a retry, the failure is visible and raises a notification.

The axes are independent: an active source can be either idle or syncing, and error speaks to the last run, not to the admin's intent. The badges on the Admin Panel · Sources and Admin Panel · Sync screens are a rendering of these axes, not a separate truth.

During a run the source is locked. While it's syncing, operations on it are unavailable — editing the configuration, pausing, disconnecting, deleting, and re-running: one source, one active run (single-flight per-source). The only available lever is Cancel.

Cancel is safe and therefore needs no rollback. The run stops at the nearest checkpoint, what's already processed sits consistently — Load is idempotent on source_id + source_type + source_entity_id, and no half-assembled state arises. A rollback to a snapshot would be harmful and unnecessary: the data self-reconciles through idempotent upsert, Incremental, and Reconciliation.

Pause, meanwhile, silences the schedule, rather than freezing a live run. Resume from a checkpoint heals a worker crash within a short window, subject to a freshness budget; a run cancelled or paused for long is terminal — the next one starts fresh instead of finishing off a stale fetch.

Deletion — the choice at confirmation
Configuration only
The source is removed, its data is orphaned but stays in Knowledge Store — in case of reconnection or if it's still in use.
Configuration + data
Behind a type-to-confirm: cascade-cleans the source's data out of the graph, vectors, and metadata. Irreversible — for full decommissioning.

Pause and Disconnect are reversible and don't touch data; the fate of what was collected rests with Delete alone. The "configuration + data" cleanup is an explicit hard teardown, unlike the soft archiving of a record that vanished from the source itself (that one is kept for audit). An entity fed by another source as well doesn't disappear here — only the deleted source's contribution is detached; cross-source stitching and its boundary are owned by data-model → Knowledge Store.