Cairn

Self-hosting Cairn

This guide takes you from a clone to a running Cairn instance. Cairn is a Turborepo monorepo with two runtime apps — the web app (Next.js, the dashboard + API) and the worker (runs Playwright tests and background jobs via Inngest) — backed by Postgres.

Heads up: Cairn integrates deeply with GitHub and uses hosted AI models, so a full instance requires you to create your own GitHub App, a GitHub OAuth App, and a Google Gemini API key. There is no zero-config path yet (see the deferred items in OPEN_SOURCE_ROADMAP.md). Budget ~20 minutes for first-time setup.


1. Prerequisites

  • Node.js 20+
  • pnpm 9corepack enable && corepack prepare pnpm@9.3.0 --activate
  • A PostgreSQL database — any Postgres 14+ works (local, Railway, Neon, Supabase, RDS…). You need a connection string.
  • Playwright/Chromium for the worker — installed automatically in the worker Docker image, or locally with npx playwright install chromium.

2. External accounts to create

ServiceWhyWhat you get
GitHub OAuth App"Sign in with GitHub"GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET
GitHub AppRead repos, file issues, open PRs, receive webhooksGITHUB_APP_ID, GITHUB_APP_SLUG, GITHUB_APP_PRIVATE_KEY, GITHUB_WEBHOOK_SECRET
Google AI StudioFlow mapping + test generation (Gemini)GOOGLE_GENERATIVE_AI_API_KEY
Inngest (or the dev CLI)Background jobs, cron, retriesINNGEST_EVENT_KEY, INNGEST_SIGNING_KEY (prod only)
Anthropic (optional)Auto-fix PR drafting (Claude)Users add their own key per environment in the dashboard

GitHub OAuth App

  1. https://github.com/settings/developersNew OAuth App.
  2. Authorization callback URL: http://localhost:3000/api/auth/callback/github (use your domain in production).
  3. Copy the Client ID and generate a Client secret.

GitHub App

  1. https://github.com/settings/appsNew GitHub App.
  2. Permissions: contents: read, issues: write, pull_requests: write, checks: write (optional).
  3. Subscribe to events: installation, installation_repositories, push, pull_request.
  4. Webhook URL: http://localhost:3000/api/github/webhook — for local dev, expose it with a tunnel (ngrok http 3000, cloudflared, etc.) and use that URL. Set a Webhook secret and copy it.
  5. Generate a private key (downloads a .pem).
  6. Note the App ID and the app slug (from the app's public URL).

3. Clone & install

git clone https://github.com/Vela-Engineering/cairn-vela.git
cd cairn-vela
pnpm install

4. Configure environment

Copy the example and fill it in:

cp .env.example .env

Key variables (see .env.example for the full, annotated list):

# Database — pooled URL for the app, direct URL for migrations
DATABASE_URL="postgresql://user:pass@host:5432/cairn?sslmode=require"
DATABASE_URL_UNPOOLED="postgresql://user:pass@host:5432/cairn?sslmode=require"

# Auth
BETTER_AUTH_SECRET="<openssl rand -base64 32>"
BETTER_AUTH_URL="http://localhost:3000"
NEXT_PUBLIC_APP_URL="http://localhost:3000"

# GitHub OAuth (login)
GITHUB_CLIENT_ID="..."
GITHUB_CLIENT_SECRET="..."

# GitHub App (repo access + webhooks)
GITHUB_APP_ID="..."
GITHUB_APP_SLUG="your-cairn-app"
GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
GITHUB_WEBHOOK_SECRET="..."

# AI model (flow mapping + test generation)
GOOGLE_GENERATIVE_AI_API_KEY="AIza..."

# Background jobs — leave keys empty for local dev (uses the keyless dev server)
INNGEST_EVENT_KEY=""
INNGEST_SIGNING_KEY=""

# Artifact storage (test videos/traces) — local volume is the simplest default
STORAGE_DRIVER="volume"
ARTIFACTS_DIR="./.artifacts"
ARTIFACTS_BEARER_TOKEN="<openssl rand -hex 32>"

# Optional: weekly recap email (Resend)
RESEND_API_KEY=""
RECAP_FROM_EMAIL="Cairn <recap@yourdomain.com>"

GITHUB_APP_PRIVATE_KEY is multi-line — keep the \n escapes (or quote the whole PEM) so it survives .env parsing.

5. Create the database schema

pnpm db:push       # pushes the Drizzle schema to DATABASE_URL
# pnpm db:studio   # optional: browse the DB

6. Run it (local dev — three processes)

Cairn runs as three processes in development:

# Terminal 1 — Inngest dev server (keyless; auto-discovers the endpoints)
npx inngest-cli@latest dev

# Terminal 2 — web app on http://localhost:3000
pnpm dev

# Terminal 3 — worker on http://localhost:3100 (needs Chromium installed)
pnpm --filter worker dev

Open http://localhost:3000, sign in with GitHub, install your GitHub App on a repo, and Cairn will start mapping flows.

Working on UI only? You can run just pnpm dev — the landing page and most of the dashboard render without the worker or a full backend.

Run the whole stack with Docker Compose

Prefer one command? The repo ships a root docker-compose.yml that runs the web app, the worker, and a bundled Postgres, with run artifacts on a local Docker volume. You bring the external service credentials in .env.

cp .env.example .env          # fill in the external creds
docker compose up --build

# apply the database schema once (from a checkout, against the exposed DB):
DATABASE_URL=postgres://cairn:cairn@localhost:5432/cairn pnpm db:push

The compose file points DATABASE_URL at the bundled Postgres and sets STORAGE_DRIVER=local so artifacts persist to a volume instead of R2. You still supply your own INNGEST_* keys, model key, GitHub App creds, and BETTER_AUTH_SECRET in .env — those stay external.

Because Inngest calls into the worker, the worker must be reachable by Inngest: run the keyless inngest-cli dev server locally, or in production expose the worker and register its /api/inngest endpoint in your Inngest dashboard.

7. The worker in production (Docker)

The worker needs Chromium, so it ships with a Playwright base image:

docker build -t cairn-worker -f apps/worker/Dockerfile .
docker run --env-file .env -p 3100:3100 \
  -v cairn-artifacts:/artifacts cairn-worker

Set ARTIFACTS_DIR=/artifacts and mount a persistent volume there so run videos/traces survive restarts. In production, set the real INNGEST_EVENT_KEY / INNGEST_SIGNING_KEY and register both /api/inngest endpoints (web + worker) in your Inngest dashboard.

8. Deployment notes

  • The web app is a standard Next.js app (Vercel, Railway, Node host).
  • The worker is a long-running Node/Express server that needs Chromium and a persistent volume — host it where you can run the Docker image (Railway, Fly.io, a VPS).
  • Point BETTER_AUTH_URL / NEXT_PUBLIC_APP_URL at your real domain and update the GitHub OAuth callback + GitHub App webhook URLs to match.
  • WORKER_ARTIFACTS_URL lets the web app proxy artifact reads from the worker when they run on different hosts; secure it with ARTIFACTS_BEARER_TOKEN.

9. Troubleshooting

  • Webhooks never arrive (local): your GitHub App webhook URL must be a public tunnel (ngrok/cloudflared), not localhost.
  • "Browser not found" in the worker: run npx playwright install chromium locally, or use the Docker image which bundles it.
  • Migrations can't connect: db:push uses DATABASE_URL / DATABASE_URL_UNPOOLED — make sure the direct (non-pooled) URL is reachable.
  • Auth redirect loops: confirm BETTER_AUTH_URL and NEXT_PUBLIC_APP_URL match the origin you're visiting (http vs https, port included).

Stuck? Open a discussion or see CONTRIBUTING.md.