Skip to main content
Version: 0.3.0

Providers

Each provider is a separate worker process in its own repository. Core never imports worker code — the contract is the proto messages + the NATS subjects (see Provider workers). You run only the workers you need.

Enrollment​

In production the Strava worker authenticates to NATS with a one-time enrollment token (NATS auth-callout mints scoped credentials). Create one as an admin. 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", "expires_in_hours": 720, "note": "prod Strava"}'
  • name (required): the worker name — lowercase letters, digits and hyphens, no leading or trailing hyphen. The worker's CAIRN_WORKER_NAME must equal it.
  • expires_in_hours (optional): defaults to 8760 (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. Pass it to the worker as CAIRN_WORKER_ENROLLMENT_TOKEN. GET /api/admin/worker-enrollments lists enrollments, 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.

Strava (Go)​

Repo: cairn-provider-strava. Strava is read-only for Cairn. Each user brings their own Strava API app (per-user OAuth) for rate-limit isolation and webhook ownership; users connect their account from the Connections screen.

docker run --rm \
-e CAIRN_NATS_URL=nats://nats:4222 \
-e CAIRN_WORKER_NAME=strava-fetcher \
-e CAIRN_WORKER_ENROLLMENT_TOKEN=... \
ghcr.io/johnnycube/cairn-provider-strava:v0.3.0

Only one Strava worker instance should own the webhook subject — set CAIRN_STRAVA_WEBHOOK_VERIFY_TOKEN on exactly one.

VariableDefaultNotes
CAIRN_NATS_URLnats://localhost:4222CAIRN_NATS_USER / CAIRN_NATS_PASSWORD for authenticated servers
CAIRN_WORKER_ENROLLMENT_TOKEN—one-time enrollment token from POST /api/admin/worker-enrollments
CAIRN_WORKER_NAMEstrava-fetchermust equal the enrollment's name
CAIRN_WORKER_INSTANCE_IDthe hostnamestable id when you run several instances
CAIRN_STRAVA_WEBHOOK_VERIFY_TOKEN—set on exactly one instance
CAIRN_STRAVA_TOKEN_URLStrava'soverride for testing against a mock
CAIRN_LOG_LEVELinfodebug, info, warn or error

Garmin (Python)​

Repo: cairn-provider-garmin. Garmin has no public activity OAuth API, so the worker uses the garminconnect library (Garth SSO). Per-user credentials are delivered over NATS. The worker does not use an enrollment token; it connects with CAIRN_NATS_USER / CAIRN_NATS_PASSWORD when set, otherwise anonymously.

docker run --rm \
-e CAIRN_NATS_URL=nats://nats:4222 \
-v garmin-tokens:/data \
ghcr.io/johnnycube/cairn-provider-garmin:v0.3.0
VariableDefaultNotes
CAIRN_NATS_URLnats://localhost:4222CAIRN_NATS_USER / CAIRN_NATS_PASSWORD are used when set
CAIRN_WORKER_NAMEgarmin-fetchershared by every instance of the worker
CAIRN_WORKER_INSTANCE_IDrandom 12-hex idunique per instance; in Kubernetes set it to the pod name so a restart reuses its presence key
CAIRN_HEARTBEAT_INTERVAL_S20presence heartbeat to the cairn_worker_presence KV
CAIRN_PULL_BATCH4jobs pulled per fetch
CAIRN_GARTH_HOME/data/garth in the image, ~/.cache/cairn-garth outside itGarth token store, one subdirectory per account; keeps the Garmin session between jobs. Mount a volume on /data so MFA accounts are not re-prompted on every restart
CAIRN_GARMIN_EMAIL / ..._PASSWORD—dev fallback only — in production each user's credentials are delivered over NATS
CAIRN_PROTO_PATH/opt/cairn-proto in the image, <repo>/proto outside itlocation of the committed proto stubs

The image runs as the unprivileged user cairn (uid/gid 10001) and declares /data as a volume. The token directory must be readable and writable by that uid: a fresh named volume or an emptyDir already is; a volume or bind mount written by an image before 0.2.3 (which ran as root) needs chown -R 10001:10001 once, and in Kubernetes a persistent volume needs securityContext.fsGroup: 10001 on the pod. Without it the worker cannot read or write cached tokens and logs in to Garmin on every job.

The worker cannot answer an MFA prompt. For MFA accounts, seed the token store once with an interactive login as described in the worker README, then mount it.

NATS permissions​

When NATS runs with auth-callout, an enrolled worker gets core's standard permission template, derived from the enrollment's provider and its worker key <name>-<provider> (for example strava-fetcher-strava). For a provider <p> and worker key <wk> it allows:

Subjects
Subscribecairn.jobs.{fetch_source,parse_blob,backfill,reconcile,import_metrics}.<p>, cairn.cmd.worker.<wk>.>, cairn.discover.<p> (serving discover requests), cairn.webhooks.<wk>.event, cairn.webhooks.<wk>.verify, _INBOX.>
Publishcairn.results.{fetch_source,parse_blob,backfill,import_metrics}.<p>, cairn.results.reconcile.<p>.* (account-suffixed), cairn.jobs.fetch_source.<p> (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>, cairn.accounts.lookup_by_provider_ext, _INBOX.>
JetStream API$JS.API.STREAM.NAMES (stream lookup by subject); consumer create on CAIRN_JOBS filtered to the five 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

The job subjects are exact: core publishes cairn.jobs.fetch_source.<p> with nothing after the provider, so consumers must filter on that subject (a cairn.jobs.fetch_source.<p>.> filter matches nothing). Core before 0.3.0 granted a narrower template under which enrolled workers could not pull jobs; run core v0.3.0 or later with NATS auth. Installations running NATS without auth are unaffected.

If you give a worker a static NATS user instead (the Garmin worker connects with 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.