Prerequisites
Install dependencies with a clean, lockfile-exact install:
APP_SECRET to the output of openssl rand -base64 32. It is required in development too — there is no built-in fallback — and it must decode to exactly 32 bytes. DATABASE_URL remains the only database setting a source checkout needs.
Localhost, 127.0.0.1, and [::1] are trusted automatically outside production, so APP_ORIGINS is only needed when you reach the dev server by another hostname, such as a tunnel or a machine name on your LAN.
One-command environment
http://127.0.0.1:3000 and the development GraphQL WebSocket runs on port 3092, so an installed Homebrew service can keep using ports 3090 and 3091. Later runs reuse the stable development-agent identity stored at:
npm run dev, register the initial user, and create a token on Agents. Then restart both processes with:
enrollAgent mutation accepts an anonymous GraphQL request. The development agent checks public /api/auth/config for readiness, then sends the one-time token to that mutation. It never creates an enrollment token anonymously.
That path is deliberately distinct from the ~/.config/control-agent/config.json a released agent uses, so a development agent and a service agent can coexist on the same Mac as two separate enrollments.
Next.js keeps hot reload, and agent source changes restart only the development agent. Open http://127.0.0.1:3000/en/agents to inspect it.
Automatic development enrollment refuses non-loopback server addresses. It is
a convenience for
127.0.0.1, not a way to skip the enrollment-token flow
against a real server.Port overrides
Every port has an environment override, which matters when the defaults are already taken:
Change
AGENT_WS_PORT and NEXT_PUBLIC_AGENT_WS_URL together. The first moves the listener; the second tells the browser where to find it. Leaving them out of sync produces a dashboard that loads but never receives live updates.
For agent-only development, CONTROL_AGENT_DEV_SERVER, CONTROL_AGENT_DEV_WEBSOCKET_SERVER, and CONTROL_AGENT_DEV_CONFIG override the local endpoints and the dedicated credential path — useful for pointing a watch-mode agent at a server you started separately.
Repository layout
src/generated/ is produced by npm run generate. It is regenerated on every build, so edits there are lost.
Commands
Running the app
npm run dev regenerates the client and applies pending migrations before starting Next.js, so a fresh checkout needs no separate setup step.
Code generation
The SDL is bundled rather than read from disk at runtime because the Homebrew service runs the Next.js
standalone output, which does not carry the schemas/ directory. codegen.ts is SDL-first: resolver types are generated from the schema, not from client documents.
Checks and tests
Tests live beside the code they cover as
*.test.ts and *.test.tsx. The suite runs under jsdom with a fixed America/New_York timezone — date formatting tests assert that a zoneless render differs from the UTC one, which is vacuously true on a UTC machine. packages/control-agent/** and playwright/** are excluded from the app’s Vitest run and have their own runners.
npm run test:coverage uses the V8 provider and includes untested application, script, and agent-contract source files in the totals. It writes a terminal summary, an HTML report, lcov.info, coverage-final.json, and coverage-summary.json under coverage/; reports are still written when tests fail. The test-coverage VS Code task runs the same command.
Database
Dependencies and packaging
The production server accepts the standard Next.js
HOSTNAME and PORT environment variables plus DATABASE_URL. It also requires APP_SECRET, and APP_ORIGINS is recommended; see Environment variables for the full list and Authentication for the password and OIDC mode variables.
The control agent
The generic TypeScript control agent lives inpackages/control-agent, and the types it shares with the server live in packages/agent-contract. Both are npm workspaces of the root package, so npm ci at the root installs them and the agent:* scripts proxy into them.
The agent is a client, not a server: it dials the control plane’s GraphQL WebSocket outbound and executes jobs it is handed. Nothing on a managed machine listens for inbound connections. It bundles the Claude Agent SDK, the Codex SDK, and the OpenCode SDK, which is how a Session actually runs a model on the target machine.
Continuous integration
.github/workflows/pull-request.yml runs three jobs against every pull request to main:
npm run full-check:ci reproduces the first job locally. Run it before opening a pull request — it is the same sequence in the same order.
Screenshots
npm run screenshots captures every app route at four combinations of viewport and colour scheme, and records a walkthrough screencast at each of them. It installs the Chromium build Playwright needs, rebuilds prisma/mock.db from the seed modules in scripts/mock-data/, produces an isolated Next output in .next-mock/, and runs the capture suite. Everything lands in screenshots/<project>/ and is gitignored — captures are generated on demand, not a committed baseline.
desktop-light, desktop-dark, mobile-light, and mobile-dark. Desktop shoots at 1920x1080 and mobile at the iPhone 13 viewport, both at a device scale factor of 2 so text and icons stay crisp when the PNGs are scaled down.
npm run screenshots:copy prompts for the docs project directory, defaulting to ../ai-development-environment-docs, and copies screenshots/desktop-light/ into its images/light/ and screenshots/desktop-dark/ into its images/dark/ — the two directories these pages swap between by theme. Pass the directory to skip the prompt:
scripts/mock-api-server.ts stubs the GitHub and Jira APIs, and the capture server points at it with the GITHUB_API_BASE_URL and GITHUB_GRAPHQL_URL overrides. Pages backed by those integrations have no local tables, so without the stub they would render their “connect your account” empty state instead of data.
Routes are listed in playwright/routes.ts, keyed to the deterministic IDs the seed writes, so detail pages always resolve to a populated record. A route fails if it returns HTTP 4xx/5xx or raises an uncaught page error; the screenshot is still written first so failures stay diagnosable.
The capture server takes an OS-selected free port, as does the agent WebSocket port the instrumentation hook binds, so a capture never collides with a dev server or another capture; the mock API stays on 4322. Set SCREENSHOT_PORT or AGENT_WS_PORT to pin either one. The server also sets a placeholder PUBLIC_BASE_URL so the device-enrollment form renders rather than disabling itself.
The mock database’s credentials are encrypted with a key derived from a fixed
APP_SECRET that the capture server passes back in. If those two ever
diverge, the app rewrites every credential row on first use and the VACUUM
that follows locks the database mid-capture.Walkthrough screencasts
playwright/walkthrough.spec.ts writes walkthrough.webm alongside the stills — a ~13s click-through of the Action Center, Worktrees, a worktree’s detail page, and Sessions, ending where it began so the docs landing page can loop it seamlessly. The stops live in playwright/walkthrough.ts, each reached by clicking either its primary navigation entry or the plain surface of a worktree card, the way a reader would. That is also what keeps the recording honest: a stop that stops being reachable fails the test rather than quietly filming the wrong page.
There is no cursor in the recording. Each click is marked by a dot that pops, holds, and fades, drawn by an init script in walkthrough.ts — Playwright’s own video overlay can draw one, but only bundled with a cursor sprite and an action label, and the option controlling how long it lingers delays every action, so a pointer move costs as much as a click. Marking clicks from inside the page also covers the taps on the mobile projects.
The desktop projects record at their full 1920x1080, the mobile ones at their viewport’s 390x664, because Playwright only ever scales a recorded frame down into the size it is asked for. The mobile screencasts are captured but not published — Mintlify has no responsive-asset story to use them with.
Related pages
APIs
The GraphQL endpoint, REST routes, and the MCP endpoint this server exposes.
Database
Prisma configuration, migrations, and reclaiming space.
Hosting and networking
Public HTTPS, reverse proxies, and Cloudflare Access paths.
Quickstart
Installing a release and enrolling an agent.