This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
- ALWAYS use
pnpm— nevernpmornpx - NEVER run
pnpm db:resetwithout asking the user first (drops all PostgreSQL tables)
pnpm dev # Dev server on localhost:3000 (host Next.js)
pnpm build # Production build
pnpm lint # ESLint
pnpm format # Prettier --write (whole repo)
pnpm format:check # Prettier --check (CI-style)
pnpm test # Unit tests (vitest)
pnpm test -- src/lib/diff # Tests in specific directory
pnpm db:push # Push schema changes to DB (scripts/migrate.js:
# plugin-table renames FIRST, then drizzle-kit
# push --force). Never use db:push:raw on a DB
# with data — bare drizzle-kit push cannot see a
# rename and DROPS the old plugin tables.
pnpm db:studio # Drizzle Studio
# Host postgres (persists in `lastest-pgdata` named volume; defined in ./docker-compose.yml)
docker compose up -d
# OCR service container (packages/ocr-service) — REQUIRED for OCR features
# (ocr-text selectors, text-region-aware diffing); the app has no in-process
# Tesseract. Part of the default compose stack; set
# OCR_SERVICE_URL=http://localhost:8891 in .env.local.
pnpm ocr:up # docker compose up -d --build ocr
pnpm ocr:down
# k3d cluster — hosts dynamically-provisioned EB Job pods only (no app, no db)
pnpm stack # create k3d cluster + build/import EB image
pnpm stack:refresh # rebuild EB image + import (alias of stack:refresh:eb)
pnpm stack:refresh:eb # same
pnpm stack:status # cluster + EB jobs/pods + host /api/health
pnpm stack:logs # tail EB pod logs (default; only EB lives in cluster)
pnpm stack:logs:eb # explicit
pnpm stack:stop # delete cluster (pnpm stack:purge also drops .k8s-secrets.yaml)
# Deploy targets (homeservers — unchanged)
pnpm deploy:olares # k8s deploy to Olares
pnpm deploy:zima # docker-compose deploy to ZimaBoard/CasaOS
pnpm deploy:npm # publish @lastest/runner
pnpm deploy:all # zima + olares + npmThe dev architecture is: pnpm dev on the host, postgres on the host (docker), and EBs provisioned dynamically as local child processes of the pool service (the default; a k3d-cluster mode exists for parity testing).
- EB pool service (
packages/pool-service/): a standalone singleton process owning the browser-capacity plane — EB provisioning, pool caps/warm pool/launch throttle, idle+stale EB reapers. The app calls it over HTTP via@lastest/pool-service/client(defaultshttp://127.0.0.1:9500, optionalEB_POOL_SERVICE_TOKENbearer auth). In dev, runpnpm dev:poolin a second terminal alongsidepnpm dev— without it, provisioning-dependent flows degrade to "no browser available". In Docker the entrypoint starts it automatically (EB_POOL_SERVICE_DISABLED=1to opt out when running it as its own k8s Deployment). Capacity accounting has no in-memory counter: the ledger is the backend itself (live k8s Jobs labeledapp=lastest-eb/ live child processes), read fresh under a provision lock inprovisionOneEB()— so it survives service restarts and can't overshootebPoolMax. - Provisioner modes (
provisionerMode()inpackages/pool-service/src/common.ts, envEB_PROVISIONER):process— default in a dev checkout (whenEB_PROVISIONERis unset ornone): the pool service spawnspackages/embedded-browseras local child processes (src/process-provisioner.ts) via tsx. No cluster, no Docker, no extra env needed beyondENCRYPTION_KEY+DATABASE_URL. Each EB gets a port block fromEB_PROCESS_PORT_BASE(default 9300, stride 20: stream=P, health=P+1, cdp=P+2, cdp-proxy=P+12) and registers back over127.0.0.1. Effective pool cap ismin(ebPoolMax, EB_PROCESS_POOL_MAX=4); warm pool defaults to 0, and builds never prewarm (prewarmForBuildis a service-side no-op in this mode) — each claim spawns exactly one EB on demand (~2-5s). Requires Playwright Chromium on the host (pnpm --filter @lastest/embedded-browser exec playwright install chromium).kubernetes— setEB_PROVISIONER=kubernetesexplicitly: one k8s Job per EB into a local k3d cluster (pnpm stack; manifests ink8s/, scripts inscripts/k3d-*.sh). WhenKUBERNETES_SERVICE_HOSTis unset the provisioner shells out tokubectl config viewand uses the current kubeconfig context (k3d-lastest). EB pods reach the host app viahost.k3d.internal:3000(CoreDNS override installed byk3d-up.sh). Also needsEB_NAMESPACE=lastest,EB_IMAGE=lastest-embedded-browser:latest,LASTEST_URL=http://host.k3d.internal:3000.none— no dynamic provisioning (static EB fleets only, e.g. Zima compose replicas). The default outside a dev checkout; force withEB_PROVISIONER=disabled.
- Tenant priority classes (kubernetes mode): EB Jobs can carry a per-tenant
priorityClassName—lastest-eb-restrictedfor free tiers (free/demo/trial),lastest-eb-unrestrictedfor paying ones (starter/growth/pro). The pool service readsteams.planfresh at provision time from theteamIdthe caller passes (provisionEB/prewarmForBuild/claimOrProvisionPoolEB); every unknown (no teamId, unknown team, DB error, warm-pool launch) falls back to restricted. Off unless enabled withEB_PRIORITY_CLASSES=1(or by settingEB_PRIORITY_CLASS_RESTRICTED/EB_PRIORITY_CLASS_UNRESTRICTED) — the API server rejects a Job naming a PriorityClass the cluster doesn't have, and the classes are created by the cluster repo, not here. - Both dynamic modes inject a per-session
EB_BOOTSTRAP_TOKEN(HMAC-signed withENCRYPTION_KEY, bound to the EB's instanceId, TTL = its deadline) — dynamic EBs never receive a fleet-wide secret (process mode also withholdsDATABASE_URL/ENCRYPTION_KEYfrom child env).SYSTEM_EB_TOKENremains accepted at auto-register ONLY for static fleets. The pool service therefore needs the sameENCRYPTION_KEYas the app. - Required
.env.localkeys for the host dev flow (process mode):ENCRYPTION_KEY=<64 hex chars>(shared by app + pool service; signs stream grants and EB bootstrap tokens)OCR_SERVICE_URL=http://localhost:8891(OCR container from docker-compose; without it OCR features are disabled)DATABASE_URL=postgresql://lastest:lastest@localhost:5432/lastestSYSTEM_EB_TOKENonly for static-fleet EBs; dynamic EBs use per-session bootstrap tokens
- All built images + cluster containers carry
com.docker.compose.project=lastestso Docker Desktop groups them as one stack. - EB stream proxy:
scripts/front-proxy.jsowns the public port (:3000) in every deployment; it spawns Next on 127.0.0.1:3001 (the command after--) and reverse-proxies HTTP to it. WebSocket upgrades for/api/embedded/stream/wsare terminated by the front proxy itself (Next never sees them — no upgrade-listener races); all other upgrades (dev HMR) tunnel through untouched. The upstream EB pod address is dynamic per-session, so this can never be a static ingress route. It authorizes upgrades with an HMAC-signed grant carrying the upstream pod address (src/lib/eb/stream-grant.ts), minted bytoProxyStreamUrl()behindrequireAuth(). Never key it on an EB-held credential (EB_BOOTSTRAP_TOKEN/ legacySYSTEM_EB_TOKEN): every EB pod holds one. The verifier is duplicated in the front proxy (a dependency-free script with no TS loader) — change both together;src/lib/eb/stream-grant.test.tscross-checks them in a child process, andsrc/lib/eb/front-proxy.test.tsexercises the proxy end-to-end. - EB stream credential: the browser holds NO stream credential — only the opaque grant, which is a capability for one pod that expires with it. The EB's stream port is guarded by
STREAM_AUTH_TOKEN, derived per instance asHMAC(ENCRYPTION_KEY→"eb-stream-auth-v1", instanceId)(deriveStreamAuthToken()inpackages/pool-service/src/common.ts, duplicated in the front proxy). The pool service injects it at provision time; the front proxy re-derives it from thei(instanceId) field of the grant and presents it as anx-stream-tokenheader, so it appears in no request line or access log.embedded_sessions.instanceIdcarries it from auto-register (where it is bound to the pod'sEB_BOOTSTRAP_TOKEN) into the grant — it is only persisted when a bootstrap token vouched for it, never when self-asserted under a static-fleetSYSTEM_EB_TOKEN. Static fleets have no provisioner and fall back to a fleet-wideSTREAM_AUTH_TOKENenv var set on both the proxy and every EB. An EB with no token configured refuses all upgrades (500) — fail closed, since a NetworkPolicy is only enforced by some CNIs and must never be the sole guard on the stream port. - EB isolation:
k8s/embedded-browser-netpol.yamlholds two policies forapp=lastest-ebpods.lastest-eb-ingressis default-deny inbound, admitting only the app pod on 9223 (stream), 9224 (health) and 9232 (CDP proxy) — never 9222 (Chromium's own localhost CDP). Enforcing inter-EB isolation on the ingress side is deliberate: the destination EB refuses the connection, so it holds regardless of the cluster's pod CIDR.lastest-eb-egressallows DNS, the in-cluster app on :3000, and the public internet minus the metadata range and the cluster CIDRs. Caveats in the file header: it is a no-op under non-enforcing CNIs (k3d/flannel —scripts/k3d-up.shdoes not apply it), theexceptCIDRs are k3s defaults that need changing per cluster, and kubelet probes pluskubectl port-forwardoriginate off-pod.
Visual regression testing platform: Next.js 16 App Router, PostgreSQL (Drizzle ORM), Playwright.
Core flow: Record browser interactions → Run tests → Diff screenshots → Review/approve baselines
Key paths:
packages/db/src/schema.ts— all tables (~3700 lines;@/lib/db/schemare-exports)src/lib/db/queries.ts— barrel re-export of all query modulessrc/lib/db/queries/— domain-focused query modules:tests.ts— tests, test runs, results, versions, assertionsareas.ts— functional areas, tree/hierarchybuilds.ts— builds, build summaries, build status, a11y score trendsvisual-diffs.ts— visual diffs, baselines, ignore regions, planned screenshotsstep-comparisons.ts— per-(build, test, step) multi-layer verdicts + evidence (v1.13)change-maps.ts— build-level Change Map (Verify phase, v1.14+)layer-baselines.ts/layer-feedback.ts— per-layer baselines + step feedback (Verify, v1.14+)repositories.ts— repos, PRs, github/gitlab accountssettings.ts— playwright, environment, diff, AI, notification settingsroutes.ts— routes, scan status, route suggestionsschedules.ts— cron-based scheduled test runsbackground-jobs.ts— background jobsauth.ts— teams, users, sessions, oauth, tokens, invitationsstorage.ts— team storage usage/quota + run-minute usage/quotabilling.ts— team billing snapshot, stripe webhook event logsetup.ts— setup/teardown scripts, configs, steps, resolutionstorage-states.ts— browser storage state managementrunners.ts— runners, runner commandsintegrations.ts— spec imports, google sheets, compose, agent sessionscsv-sources.ts— CSV test-data sourcesfixtures.ts— test fixturesgamification.ts/awards.ts— seasons, Bug Blitz, leaderboard scoring; repo awardsactivity-events.ts— activity events + live SSE broadcastlaunch.ts— launch cohorts/gatingpublic-shares.ts/demo-notes.ts— public/r/<slug>share links + AI demo notesinspector.ts— inspector cacheanalytics.ts— usage analyticsmisc.ts— selector stats, bug reports, review todos
src/lib/execution/executor.ts— test executor (~1800 lines)src/lib/verify/— check-modes system: 9 layers (visual, text, dom, network, console, a11y, design, perf, url) × enforce/log/disable; case-status derivationsrc/lib/design-system/— design-token comparison engine (the "design" check layer)src/lib/url-diff/— URL trajectory capture + diffing, rate-limit, SSRF guardssrc/lib/billing/— Stripe billing: plans, live catalog, webhook syncsrc/lib/playwright/— recorder, runner, server manager, OCR, assertion-parser, selector-analysis ("Analyze URL")src/lib/diff/— pixelmatch diffing + SHA256 baseline hashingsrc/lib/ai/— AI providers: claude-cli, openrouter, claude-agent-sdk, anthropic-direct, openai, ollama + failure-triagesrc/lib/a11y/— WCAG 2.2 AA compliance scoring (wcag-score.ts)src/lib/scheduling/— cron parser + scheduler for automated test runssrc/server/actions/— server actions for all domain opssrc/lib/ws/— runner-channel server plumbing (registry, event fan-out, step state)packages/db/— drizzle schema + Postgres client (@lastest/db), shared by app and pool service;src/lib/db/{index,schema}.tsare re-export shimspackages/pool-service/— EB pool service (separate process,pnpm dev:pool): k8s/process provisioning, pool caps, warm pool, EB reapers; app consumes@lastest/pool-service/clientpackages/eb-protocol/— canonical wire protocol app ↔ runners (@lastest/eb-protocol): command/response messages, stream messages, persisted jsonb payload shapes (schema.ts re-exports these)packages/runner/— remote runner CLI (npm package via tsup)packages/mcp-server/— MCP server for AI agent integration (@lastest/mcp-server)packages/embedded-browser/— containerized browser with CDP live streamingpackages/ocr-service/— Tesseract OCR container, the ONLY OCR backend (no in-process Tesseract in the app); app-side facade insrc/lib/ocr/requiresOCR_SERVICE_URL— unset means OCR features are disabled. The service wakes on demand and auto-sleeps after idlepackages/vscode-extension/— VS Code extension (esbuild)plugins/ci/— CI provider integration (@lastest/plugin-ci): GitHub Actions workflow + GitLab pipeline config, YAML generation, deployment to the customer's repo, setup validation. Ownsci_github_action_configs/ci_gitlab_pipeline_configs. The OAuth/token/webhook-verification half ofsrc/lib/{github,gitlab}stayed in core — seedocs/architecture/ci-migration-result.md
@better-auth/stripeplugin wired insrc/lib/auth/auth.ts; no-op whenSTRIPE_SECRET_KEYis unset (self-hosted stays free)- Plans:
free/starter/growth/pro(+ legacydemo/trial), monthly + yearly — defined insrc/lib/billing/plans.ts; run-minute quotas + project limits per tier - Live catalog fetched from Stripe (
src/lib/billing/catalog.ts, 10-min TTL, webhook-invalidated); static fallback fromplans.tswhen Stripe unreachable subscriptionstable is plugin-managed — read-only from app code;stripe_webhook_eventsis the app-owned idempotency/forensic log- Webhooks flip
teams.planimmediately (no admin gate) viasrc/lib/billing/webhook-sync.ts; upgrades prorate now, downgrades apply at period end via Subscription Schedule - Provision/refresh the Stripe catalog + portal config:
STRIPE_SECRET_KEY=sk_test_... node scripts/stripe-provision-test.mjs(re-runnable; re-run after flippingEARLY_ADOPTER_PRICING) - Env:
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET,STRIPE_AUTOMATIC_TAX(optional),EARLY_ADOPTER_PRICING(defaulttrue)
- Edit
packages/db/src/schema.ts(src/lib/db/schema.tsis a re-export shim) - Update
DEFAULT_*constants at top of schema for new settings fields - Run
pnpm db:push - Update queries in the relevant
src/lib/db/queries/*.tsmodule (barrel re-exported fromqueries.ts)
- UI: shadcn/ui (New York) + Tailwind CSS v4 (CSS-first, OKLCH colors,
@theme inlineinglobals.css) + lucide-react icons + sonner toasts - Imports: always
@/alias, never relative - Client components: named
*-client.tsx - Server actions: call
revalidatePath()after mutations; userequireRepoAccess()/requireTeamAccess()for auth - Auth guards:
requireAuth(),requireTeamAccess(),requireRepoAccess(),requireAdmin()insrc/lib/auth/ - Auth: better-auth for UI (email/password + GitHub/GitLab/Google OAuth); DB-backed session tokens (
verifyBearerToken()) for programmatic API access - Image processing:
pngjs+pixelmatch— do NOT usesharp - Password hashing:
@node-rs/argon2(not bcrypt) - Settings auto-save: 500ms debounce — when adding fields, update
originalValues,hasChanges,doSave, anduseEffectdeps - AI settings:
getAISettings()returnsDEFAULT_AI_SETTINGSwhen no DB record — all new fields must be in the default - Logging:
import { getLogger } from "@/lib/logger"(pino) in server code —const log = getLogger("GC"); log.warn({ runnerId }, "stale runner"). Production writes newline-delimited JSON to stdout; dev renders short readable lines.LOG_LEVELoverrides the level. Server-only — never import it from a*-client.tsx. Existing bareconsole.*calls still work:src/lib/logger-console-bridge.tspatches console onto pino at boot (production only, fromsrc/instrumentation.ts), lifting a leading[Prefix]intoscopeandErrorargs intoerr. - Schema types: use
$inferSelect/$inferInsertpatterns - Monorepo: pnpm workspaces, pnpm 10.x
- pnpm config:
overrides/onlyBuiltDependencieslive inpnpm-workspace.yaml— never in apackage.jsonpnpmblock (deprecated) - Formatting/lint: husky pre-commit runs
lint-staged→prettier --writethenpnpm eslinton staged files. Prettier auto-formats (and re-stages) on every commit — never--list-different/--checkin.lintstagedrc.json, that only checks and blocks the commit instead of fixing.
VisualDiffWithTestStatustype must stay in sync withgetVisualDiffsWithTestStatusquery select- Test code signature:
export async function test(page, baseUrl, screenshotPath, stepLogger)— runner strips TS annotations - OCR always goes through the
src/lib/ocrfacade, which talks HTTP topackages/ocr-service(OCR_SERVICE_URLrequired; no in-process tesseract.js in the app). tesseract.js v6+ only returnstextby default; bbox/word data needs the explicit{ blocks: true }output option (handled inside the service). - Docker entrypoint runs
drizzle-kit push --forceon startup