Cairn

Architecture

This page is for contributors. It maps the codebase so you can find where things live and understand how a change flows through the system.

Big picture

Cairn is a Turborepo + pnpm monorepo with two runtime apps and a set of shared packages:

┌─────────────┐     events      ┌──────────────┐
│   apps/web   │ ───────────────▶│ apps/worker  │
│ Next.js 16   │   (Inngest)     │ Inngest fns  │
│ dashboard +  │◀─────────────── │ + Playwright │
│ API + landing│   DB / artifacts│  runner      │
└──────┬───────┘                 └──────┬───────┘
       │                                │
       ▼                                ▼
   Postgres (Drizzle)            Artifact storage
                                 (volume / R2)
       ▲                                ▲
       └──────── GitHub App / OAuth ─────┘
                 AI models (Gemini, Claude)
  • apps/web — the Next.js App Router dashboard, the landing page, and all HTTP API routes (GitHub webhook, repo/run/issue endpoints, instrumentation ingest). It sends Inngest events but does not run Playwright.
  • apps/worker — an Express + Inngest server that runs the heavy, long-lived work: mapping, test generation, suite runs (Playwright/Chromium), auto-fix, security scans, and the weekly-recap cron. Ships with a Playwright Docker base image.
  • Postgres via Drizzle is the shared source of truth. Inngest is the durable job queue/cron that decouples web from worker.

Monorepo layout

apps/
  web/        Next.js dashboard + landing + API routes + GitHub webhook
  worker/     Inngest functions: mapping, test-gen, runSuite, autoFix,
              security, weeklyRecap; Express server for /artifacts
packages/
  db/         Drizzle schema + client (Postgres). One folder per table.
  auth/       Better-Auth server + client (GitHub OAuth)
  runner/     Playwright execution, GitHub App helpers, issue/drift writers,
              test-prompt builders, weekly-recap engine
  storage/    Artifact storage factory (volume / R2)
  api-capture/ HAR parsing, schema fingerprinting, drift diffing
  instrument/ Optional production API-capture SDK (dogfooding)
  security/   Security recon wordlists + helpers
  ui/         Shared shadcn/ui components
  *-config/   Shared eslint / prettier / tailwind / tsconfig presets

Key flows through the code

A push arrives

  1. GitHub sends a webhook to apps/web/app/api/github/webhook/route.ts (handlePush), which verifies the signature and decides if the branch is tracked.
  2. It emits an Inngest event cairn/sync.requested.
  3. apps/worker/src/functions.ts → syncChanges diffs the commit range, re-extracts affected flows, regenerates their tests, and emits a scoped cairn/suite.requested to run just the touched flows.

A suite run

  1. runSuite (worker) loads the env's flows, creates a runs row, and executes each flow with packages/runner/src/run-playwright.ts (spawns playwright test, captures video/trace/HAR, caps runtime under the platform edge timeout).
  2. Results are written to test_results; network captures go through packages/api-capture to detect contract drift.
  3. On failure, packages/runner/src/file-issue.ts drafts a root-caused issue; if auto-fix is enabled it emits cairn/fix.requested.

Auto-fix

apps/worker/src/auto-fix.ts loads the issue + failing result, reads the relevant source files, prompts Claude to diagnose and patch, then opens a PR via the GitHub App and records a fix_attempts row.

Weekly recap

apps/worker/src/weekly-recap.ts runs on a Monday cron, computes per-app metrics via packages/runner/src/weekly-recap.ts, renders an HTML digest, and (optionally) emails it via Resend.

Data model

Drizzle schemas live in packages/db/src/schema, one file per table — e.g. repositories, environments, flows, runs/testResults, issues, fixAttempts, apiEndpoints/apiFingerprints/apiDriftEvents, security*, weeklyRecaps. Most domain objects are scoped by (repoId, rootPath, branch) to support monorepos and multi-branch tracking.

Schema changes: edit the schema file, then pnpm db:generate (migration) or pnpm db:push (dev).

Conventions

  • TypeScript strict, Prettier-formatted, ESLint shared config.
  • Background work goes through Inngest functions (deterministic control flow, retries) rather than ad-hoc async in request handlers.
  • The web app talks to the worker only via Inngest events and the bearer-secured /artifacts proxy — never direct calls.

See Contributing to set up a dev environment.