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 9 —
corepack 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
| Service | Why | What you get |
|---|---|---|
| GitHub OAuth App | "Sign in with GitHub" | GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET |
| GitHub App | Read repos, file issues, open PRs, receive webhooks | GITHUB_APP_ID, GITHUB_APP_SLUG, GITHUB_APP_PRIVATE_KEY, GITHUB_WEBHOOK_SECRET |
| Google AI Studio | Flow mapping + test generation (Gemini) | GOOGLE_GENERATIVE_AI_API_KEY |
| Inngest (or the dev CLI) | Background jobs, cron, retries | INNGEST_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
- https://github.com/settings/developers → New OAuth App.
- Authorization callback URL:
http://localhost:3000/api/auth/callback/github(use your domain in production). - Copy the Client ID and generate a Client secret.
GitHub App
- https://github.com/settings/apps → New GitHub App.
- Permissions:
contents: read,issues: write,pull_requests: write,checks: write(optional). - Subscribe to events:
installation,installation_repositories,push,pull_request. - 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. - Generate a private key (downloads a
.pem). - 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_KEYis multi-line — keep the\nescapes (or quote the whole PEM) so it survives.envparsing.
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_URLat your real domain and update the GitHub OAuth callback + GitHub App webhook URLs to match. WORKER_ARTIFACTS_URLlets the web app proxy artifact reads from the worker when they run on different hosts; secure it withARTIFACTS_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 chromiumlocally, or use the Docker image which bundles it. - Migrations can't connect:
db:pushusesDATABASE_URL/DATABASE_URL_UNPOOLED— make sure the direct (non-pooled) URL is reachable. - Auth redirect loops: confirm
BETTER_AUTH_URLandNEXT_PUBLIC_APP_URLmatch the origin you're visiting (http vs https, port included).
Stuck? Open a discussion
or see CONTRIBUTING.md.