Providers
Each provider is a separate worker process in its own repository. Core never imports worker code: the contract is the proto messages plus the NATS subjects (see Provider workers). You run only the workers you need. Two providers ship today:
- Strava, a Go worker. Users bring their own Strava API app; imports arrive by polling and, optionally, push webhooks.
- Garmin, a Python worker. Users enter their Garmin credentials; the worker imports activities, gear and daily health metrics.
This page covers what is common to every provider: how a worker fits into core, how it is enrolled and authenticated, and how workers and connections are managed.
How a provider fits in
- Workers and pools. A worker runs under a name (
CAIRN_WORKER_NAME, for examplestrava-fetcher). All instances with that name form one pool; they share one durable consumer per job subject, so adding an instance adds throughput. Jobs, discovery requests and results travel on per-pool NATS subjects,cairn.jobs.<job>.<provider>.<name>and friends. Several pools can serve one provider, for example two Strava workers of different versions. - Connections. A user links a provider account on the Connections page. Each connection belongs to one pool; when a provider has several, the user picks one and can move the connection later. The page shows the sync status of every account: imported and provider totals, the watermark, rate budgets, queue counts, and whether sources are out of date.
- Sync. The scheduler reconciles every connected account on
CAIRN_SCHEDULER_POLLING_INTERVAL; the user can also start a full sync (with a preview of what it would fetch), sync gear, toggle auto import, set a poll interval and a label, and re-parse out-of-date sources from the archive without spending provider quota. Provider webhooks arrive at core (POST /webhooks/{provider}) and are forwarded to the pool over NATS. - Versions. Core publishes its release to the
cairn_coreKV bucket; a worker refuses an incompatible core (samemajor.minorbefore 1.0). Each source records the parser version that produced it; a newer parser in the pool flags the source "newer parser available" on the Connections and Manage pages. - Rate limits. Budgets are tracked per provider app and shown per connection, so one user's backfill cannot starve another's app.
Enrollment
In production each worker authenticates to NATS with a one-time enrollment token (NATS auth-callout mints scoped credentials; set it up as described in NATS auth-callout). Create an enrollment as an admin, in the admin UI (Worker onboarding) or over the API. The request is authenticated like any other API call: an admin's session cookie or personal access token.
curl -X POST https://cairn.example/api/admin/worker-enrollments \
-H "Authorization: Bearer cairn_pat_..." \
-H "Content-Type: application/json" \
-d '{"name": "strava-fetcher", "provider": "strava", "expires_in_hours": 720, "note": "prod Strava"}'
name(required): the worker name — lowercase letters, digits and hyphens, no leading or trailing hyphen. The worker'sCAIRN_WORKER_NAMEmust equal it.provider(required): the provider type the worker serves, set by the admin. It scopes the worker's NATS permissions. Any well-formed type is accepted, not onlystravaandgarmin: 1–48 lowercase letters, digits, hyphens or underscores ([a-z0-9_-]), so it can't carry NATS wildcards or subject separators. A missing or malformed provider is answered with400(provider required — lowercase letters, digits, hyphens, underscores). Since core v0.3.2; before that only the built-in providers were accepted.expires_in_hours(optional): defaults to8760(365 days), which is also the cap.note(optional): free text shown in the admin list.
The 201 response carries the enrollment record and the plaintext token,
shown once. The worker's configuration is CAIRN_WORKER_NAME (the name)
plus the token from the admin as CAIRN_WORKER_ENROLLMENT_TOKEN.
GET /api/admin/worker-enrollments lists enrollments and providers, the
built-in providers as suggestions (the admin UI offers them in the provider
field, which also takes any other well-formed type),
POST /api/admin/worker-enrollments/{id}/revoke revokes one and
POST /api/admin/worker-enrollments/{id}/prolong moves its expiry, even after
it has lapsed. Both take an optional JSON body: {"reason": "..."} for revoke,
{"expires_in_hours": 720} for prolong, with the same default and cap of
8760. The body can be left out; one that is not valid JSON is answered with
400.
Other providers
A worker for a provider core does not ship (say polar) is enrolled the same
way: the admin creates an enrollment with "provider": "polar" and hands the
worker its name and token. It is admitted and scoped to the polar subjects
only (cairn.jobs.*.polar.<name>, cairn.tokens.polar.>, …), never another
provider's or another pool's. Several workers with different names can serve
one provider; each connection is then assigned to one of them on the
Connections page. Linking user accounts for a new provider still needs core
support: OAuth connect and job dispatch use core's built-in provider list.
Re-enroll required
Before core v0.3.1 an enrollment carried no provider. Core refuses those at
connect (reason enrollment_missing_provider) and cannot fill the provider in
afterwards. The enrollment list marks them with "reenroll_required": true
and the admin UI with a re-enroll required badge: create a new enrollment
with a provider, give the worker its token, and revoke the old one.
NATS auth-callout
Core answers NATS auth-callout requests when CAIRN_NATS_AUTH_CALLOUT=true.
Core and the admitted workers share one NATS account, named by
CAIRN_NATS_WORKER_ACCOUNT; core connects as a user that bypasses the
callout. Worker enrollment under NATS auth requires core v0.3.1 or later.
-
Bootstrap the account signing key once:
POST /api/admin/nats/bootstrap-account-keyreturns itspublic_key. -
Run nats-server with this config (it is
docker/nats-auth-callout.confin cairn-core).$CAIRN_NATS_AUTH_ISSUER(the public key) and$CAIRN_NATS_PASSWORDare read from nats-server's environment.http_port: 8222jetstream { store_dir: "/data" }max_payload: 8MBaccounts {CAIRN {jetstream: enabledusers: [# core itself; listed in auth_users, so it bypasses the callout{ user: cairn-core, password: $CAIRN_NATS_PASSWORD }]}}authorization {auth_callout {issuer: $CAIRN_NATS_AUTH_ISSUERaccount: CAIRNauth_users: [ cairn-core ]}} -
Start core with
CAIRN_NATS_USERNAME=cairn-core, the sameCAIRN_NATS_PASSWORD,CAIRN_NATS_AUTH_CALLOUT=trueandCAIRN_NATS_WORKER_ACCOUNT=CAIRN. Core does not start if the auth-callout is on and the account is missing. -
Create an enrollment per worker (its
nameequal to the worker'sCAIRN_WORKER_NAME) and setCAIRN_WORKER_ENROLLMENT_TOKENon it. The Strava and Garmin workers (v0.3.1 or later) both present only the token on CONNECT.
Strava workers up to v0.3.0 also present an nkey and fail unless nats-server
sends a connect nonce. For them, add a seedless placeholder nkey user to the
CAIRN users, e.g.
{ nkey: UBZQLXX3CXHIQZ2VZ4WSKLWB3DOT6ZI4WMLTQHCFPRTFDQ2A7PPABH44 } (generate
your own with nk -gen user -pubout); core's docker/nats-auth-callout.conf
still contains one. Strava v0.3.1 and Garmin v0.3.1 do not need it.
A worker can still use a static NATS user (CAIRN_NATS_USER /
CAIRN_NATS_PASSWORD) instead, for example on a server without the
auth-callout; under the auth-callout add that user to the CAIRN account and
to auth_users. Grant it the permissions for its
provider.
Managing workers
Administrators see the fleet in the admin area and on /api/admin/*:
- Workers (
GET /api/admin/workers): every live instance with its presence heartbeat, release and manifest (the data types and jobs it declares). A manifest newer than what a source was parsed with is what flags the source for re-parse. - Pools (
GET /api/admin/worker-pools): registered pools per provider, the package bound to each name, and retire for a pool that is gone for good (POST /api/admin/worker-pools/{provider}/{name}/retire). - Enrollments (
GET /api/admin/worker-enrollments): revoke and prolong, see Enrollment. - Health (
GET /api/admin/health) grades the fleet: live instances with a compatible release are healthy, an incompatible release is degraded, a registered pool without a live instance is offline. The admin event stream reports workers registering, going offline, deregistering or becoming degraded, and dead-lettered jobs, see Administration. - Import queue and dead letters (
/api/admin/jobs,/api/admin/dlq): retry, cancel and replay, see Administration.
NATS permissions
When NATS runs with auth-callout, an enrolled worker gets core's standard
permission template, derived from the enrollment's provider <p>, its worker name <n> and the
worker key <wk> = <n>-<p> (for example strava-fetcher-strava). It allows:
| Subjects | |
|---|---|
| Subscribe | cairn.jobs.{fetch_source,parse_blob,backfill,reconcile,import_metrics,import_gear}.<p>.<n> (its pool, plus the pre-0.5 subjects without .<n> so an upgraded pool drains jobs queued before the upgrade; still granted in 0.6.0), cairn.cmd.worker.<wk>.>, cairn.discover.<p>.<n> (serving discover requests), cairn.webhooks.<wk>.event, cairn.webhooks.<wk>.verify, _INBOX.> |
| Publish | cairn.results.{fetch_source,parse_blob,backfill,import_metrics,import_gear}.<p>.<n>, cairn.results.reconcile.<p>.<n>.* (account-suffixed), cairn.accounts.lookup.<p>.<n>, cairn.jobs.fetch_source.<p>.<n> (sub-jobs from reconcile and webhooks), cairn.workers.<wk>.>, cairn.tokens.<p>.>, cairn.creds.<p>.get, cairn.blobs.presign_upload.<p>, cairn.blobs.presign_download.<p>, _INBOX.> |
| JetStream API | $JS.API.STREAM.NAMES (stream lookup by subject); consumer create on CAIRN_JOBS filtered to the job subjects above and on CAIRN_WEBHOOKS filtered to cairn.webhooks.<wk>.event, plus $JS.API.CONSUMER.DURABLE.CREATE.CAIRN_JOBS.*; consumer info, MSG.NEXT and $JS.ACK.<stream>.> on both streams; stream info, direct get, message get and $KV.<bucket>.> on the cairn_worker_presence, cairn_worker_manifests and cairn_rate_limits KV buckets; stream info and gets (read-only) on cairn_core, which holds core's release |
The job subjects are exact: consumers filter on
cairn.jobs.<job>.<p>.<n>, and CAIRN_JOBS (a work queue) refuses a second
consumer with the same filter, so all instances of one pool share one durable
per subject. Run core v0.3.1 or
later with NATS auth; earlier versions did not admit enrolled workers.
Installations running NATS without auth are unaffected.
If you give a worker a static NATS user instead (CAIRN_NATS_USER /
CAIRN_NATS_PASSWORD), grant that user the same subjects for its provider.
Writing your own provider
A provider only needs to: register via enrollment, consume the
cairn.jobs.<job_type>.<provider> subjects, and publish
cairn.worker.v1.JobResult events, staying within the
permissions above. Use
the committed proto stubs (Go or Python) as your contract. See
Provider workers.