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
- 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. - It emits an Inngest event
cairn/sync.requested. apps/worker/src/functions.ts → syncChangesdiffs the commit range, re-extracts affected flows, regenerates their tests, and emits a scopedcairn/suite.requestedto run just the touched flows.
A suite run
runSuite(worker) loads the env's flows, creates arunsrow, and executes each flow withpackages/runner/src/run-playwright.ts(spawnsplaywright test, captures video/trace/HAR, caps runtime under the platform edge timeout).- Results are written to
test_results; network captures go throughpackages/api-captureto detect contract drift. - On failure,
packages/runner/src/file-issue.tsdrafts a root-caused issue; if auto-fix is enabled it emitscairn/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
/artifactsproxy — never direct calls.
See Contributing to set up a dev environment.