Skip to main content
Version: 0.7.1

Self-host overview

Cairn is built to be self-hosted. A production deployment is:

  • cairn-core — one distroless container. It serves the Connect-RPC + REST API and the embedded SvelteKit web UI, runs migrations, and is the server side of the NATS control plane.
  • Postgres 17 with TimescaleDB (streams) and PostGIS (segments).
  • NATS + JetStream — the worker control plane and async job/result bus.
  • An S3-compatible object store (MinIO, Garage, AWS S3, …) for raw blobs, photos and claim-checked worker results. Required: without it provider sync dead-letters. CAIRN_STORAGE_DRIVER=none is the explicit opt-out for API-only instances, see Choosing a store.
  • Provider workers — one container per provider you use (cairn-provider-strava, cairn-provider-garmin), each a separate process.
+-------------------+
web UI ───▶ | cairn-core | ──▶ Postgres (Timescale + PostGIS)
API ───▶ | (API + web UI) | ──▶ S3 (blobs, photos)
+---------+---------+
│ NATS + JetStream (jobs / results / tokens)
┌───────────┴───────────┐
▼ ▼
cairn-provider-strava cairn-provider-garmin

Choose a path​

Docker Compose​

The Docker Compose page has a configurator: pick the providers, object storage, reverse proxy and worker authentication you want and copy the resulting docker-compose.yml and .env. Pin the image tags to a release rather than latest, and upgrade core and the workers together.

Upgrading to 0.7.1​

Replace the core image; no migration runs. The Strava and Garmin workers 0.7.1 are built on the new provider SDKs and behave as 0.7.0 does: upgrade them when convenient, the 0.7.0 workers keep working against core 0.7.1.

  • Worker metrics are kept. Core 0.7.0 stored only the six daily health types (Steps, HRV, RestingHR, Sleep, Weight, WaterIntake) and dropped every other metric key a worker sent. 0.7.1 stores the others in the metrics store (GET /api/metrics) with the worker as their source. A metric core still refuses is logged at Warn as worker metric refused and counted in cairn_worker_metrics_refused_total.
  • Health samples carry the worker's id. The first re-import replaces the rows stored before, so the health series gains no duplicates.

Upgrading to 0.7.0​

Core 0.7 needs 0.7 workers (the usual major.minor rule), and this time the other way round too: 0.6 workers are refused the pre-0.5 job subjects they still subscribe to and cannot answer the OAuth code exchange.

  • Provider knowledge moved to the workers. Core no longer compiles in Strava's or Garmin's OAuth endpoints, scopes or form labels; each worker declares them in its manifest (provider_meta) and core stores them in the new providers table (migration 00075, run automatically). The migration seeds Strava and Garmin with the 0.6 values, so existing connections keep working while you roll the workers.
  • OAuth connects go through the pool. Connecting a Strava account now needs a live strava pool: it exchanges the authorization code. With no pool online the Connections page reports no_worker.
  • Webhook URLs are unchanged but documented correctly now: /webhooks/<worker name>-<provider>.
  • Health metrics import on their own. Accounts of a pool that declares a metric type (Garmin) get an import_metrics job once a day alongside reconcile; the manual button stays.
  • Stricter worker isolation. Core refuses token, credential and blob requests that name an account or key of another provider; a worker that kept to its own data notices nothing.

Upgrading to 0.6.1​

  • Object storage is required. Core now refuses to start when CAIRN_STORAGE_ACCESS_KEY_ID / CAIRN_STORAGE_SECRET_ACCESS_KEY are unset, instead of coming up without a blob store, a state in which every worker result dead-lettered since 0.5.0. Deployments from the Compose configurator or the Kubernetes bundle already set them. An instance that really has no store sets CAIRN_STORAGE_DRIVER=none; the opt-out is logged at Error and shown as Degraded in the health probe. See Storage.

Upgrading to 0.6.0​

  • No worker upgrade is required. The worker contract is unchanged since 0.5.0, so 0.5.0 workers keep working against core 0.6.0. The 0.6.0 worker images were released alongside core and differ only in build tooling; upgrade them when convenient, core first as always.

  • Migrations 00067–00073 run on start: OAuth client software metadata, photo variants, reimport previews, the passkey second factor, login challenge purposes, device codes and persisted maintenance jobs.

  • Forced password change. A password that was set by an administrator (the bootstrap admin, accounts created with an initial password, admin resets) must be replaced at the next password sign-in. Expect the prompt on your own admin account after the upgrade. Passkey and OIDC sign-ins are not affected.

  • Sign-in lockout. Ten failed password attempts per account or twenty per address within ten minutes lock that key for ten minutes. Every failure is in the audit log and a lock raises an admin event, see Administration.

  • OAuth scopes are enforced per family on Connect. A token now needs activities:read for ActivityService, gear:* for GearService and so on, and QueryMetricSeries needs training:read instead of training:write. Tokens issued before 0.6.0 keep the families they hold; clients re-authorize to pick up the new read scopes. REST keeps the coarse read/write rule. See OAuth.

  • Photos get display variants (320 px thumbnail, 1280 px medium, a blurhash). New uploads and mirrored photos get them on ingest; convert the existing ones once:

    docker compose run --rm core attachments backfill-variants # --batch 100
  • New optional settings: CAIRN_STORAGE_PUBLIC_ENDPOINT hands clients presigned object-store URLs instead of serving binary content through core, and CAIRN_AUTH_OAUTH_DEVICE_CODE_TTL / ..._DEVICE_POLL_INTERVAL tune the new device authorization grant. See Configuration.

  • Core now also speaks cleartext HTTP/2 (h2c) for native Connect and gRPC clients, and streaming RPCs are exempt from CAIRN_HTTP_WRITE_TIMEOUT.

  • Administrators get instance-wide maintenance operations (segment rematch, best-effort, merge and training-load recomputes, purge of soft-deleted activities, blob garbage collection), see Administration.

Upgrading to 0.5.0​

  • Core first, then both workers to 0.5.0. Jobs, results and the account lookup moved to per-pool subjects (cairn.jobs.<job>.<provider>.<worker_name>, see Provider contract); a 0.4 worker keeps running against core 0.5 but receives no new jobs. Jobs queued before the upgrade are drained by the 0.5 workers.
  • Migrations 00064–00066 add the parser/release stamps on sources, one connection per worker pool per provider account, and the worker_pools registry (filled from existing sources). Existing connections are assigned to their pool automatically when the provider has one.
  • Every source imported before 0.5.0 is flagged "newer parser available"; use Re-parse N out of date on Connections to bring them up to date without provider quota. Garmin activities need a re-fetch to pick up gear: their archives have none.
  • Rate budgets are now per provider app and shown per connection.
  • Core publishes its release to the cairn_core KV bucket and workers refuse an incompatible one. Before 1.0 the minor must match; from 1.0 on, minor and patch releases of core stay compatible with older workers.

Upgrading to 0.4.0​

  • Core first, then the workers. Core 0.4.0 ignores unknown fields in job results; a 0.4.0 worker talking to a 0.3.x core would fail every import. Core 0.4.0 with 0.3.x workers is fine (no gear, nothing else changes).
  • Migration 00063 adds gear tables and activities.gear_set_by; gear that was attached before (through the API) is kept and marked as set by the user.
  • Gear from connected accounts arrives on the next reconcile or "Sync gear" as pending records; users decide per record whether it is new gear or an alias (see Gear). Strava activities imported before 0.4.0 get their gear after a reparse, Garmin activities after a reimport.
  • The Garmin worker makes one extra call per fetched activity for its gear; set CAIRN_GARMIN_FETCH_ACTIVITY_GEAR=false to keep the old call count.

Upgrading to 0.3.3​

  • The project moved to the opencairn-org GitHub organization. Pull the images from ghcr.io/opencairn-org/ (cairn-core, cairn-provider-strava, cairn-provider-garmin); the ghcr.io/johnnycube/ namespace keeps the tags published so far as a frozen mirror and receives no new versions. Update image pins and Renovate rules; nothing else to migrate.

Upgrading to 0.3.2​

  • Worker enrollments accept any well-formed provider type (1–48 of [a-z0-9_-]), not only strava and garmin; the admin UI suggests the built-in ones. Nothing to migrate. Account linking for a new provider still needs core support (see Other providers).

Upgrading to 0.3.1​

  • Creating a worker enrollment requires a provider. Enrollments made before 0.3.1 have none; core refuses them at connect and the admin list marks them re-enroll required. Create new ones (see Providers).
  • Core answers NATS auth-callout requests only with CAIRN_NATS_AUTH_CALLOUT=true and CAIRN_NATS_WORKER_ACCOUNT set; follow the auth-callout setup. Without NATS auth nothing changes.
  • With the auth-callout, run Strava v0.3.1 and Garmin v0.3.1: both enroll with CAIRN_WORKER_ENROLLMENT_TOKEN alone. The placeholder nkey user in nats.conf is only needed for Strava v0.3.0 and earlier, and the Garmin worker no longer needs a static NATS user.

Upgrading to 0.3.0​

  • Configuration is read only from CAIRN_-prefixed variables. Unprefixed host variables (URL, USERNAME, PASSWORD, SESSION_SECRET, …) no longer fill Cairn settings; rename any you relied on.
  • An empty CAIRN_DATABASE_URL, CAIRN_AUTH_SESSION_SECRET or CAIRN_AUTH_MASTER_ENCRYPTION_KEY now fails startup. Run cairn config help to see every variable with its default.
  • Under NATS auth, upgrade core first: the worker permission template that auth-callout grants now matches the job subjects and JetStream API calls the workers make (see Providers). Workers under NATS without auth are unaffected.
  • The Garmin worker image no longer runs as root; see the volume note above.
  • Connect-RPC requests are capped at 4 MiB per message and upload bodies are capped (see Request limits).