Cloudflare OS development uses pnpm run-local to spin up the full stack locally (wrangler, workerd, frontend), and relies on Wrangler's local test harness to run integration tests against real Workers in workerd with injected dependencies rather than mocks. The router gates requests to workshop-backend (the primary Worker) and frontend assets or Vite dev server; local development, testing, and deployment each configure different bindings and asset handling via wrangler.jsonc and environment variables. Cap'n Web is a WebSocket-based RPC protocol derived from Cap'n Proto, used as the transport layer for all client-to-Worker communication in Cloudflare OS. tsgo is a Go-based rewrite of the TypeScript compiler that is substantially faster than tsc but may surface previously-suppressed type errors or behave differently on edge cases.
The quickest way to run the cloudflare-os codebase locally is pnpm run-local, which starts the full stack on wrangler and workerd and serves it at http://localhost:8787.[[1]](https://github.com/cloudflare/cloudflare-os/blob/859a56f1f683de74c46c882801325e45bb759a57/README.md) During local development, Wrangler launches a local Chrome instance to emulate the Browser Run API for the BROWSER binding; add "remote": true to the browser config block to use a remote Cloudflare browser instead.[2] The router doubles as the dev router when pnpm dev-server is used: with no ASSETS binding it proxies frontend requests to the Vite dev server instead.[3] Vite configurations across all gatekeeper packages were restructured to reduce pnpm dev-server startup time. To run public-service mode locally, a .dev.vars file must supply PUBLIC_BASE_URL, ENABLE_CLOUDFLARE_LIMITS, AUTH_GATEKEEPERS, and OAuth client ID/secret pairs for each configured gatekeeper.[4] scripts/run-dev-server.ts automatically seeds Linear and Spotify OAuth credentials into the local development environment at startup, eliminating manual credential configuration for engineers developing those gatekeeper integrations.
workshop-backend is built with pnpm run build:worker; Wrangler watches the src directory for changes during development.[2] Worker type definitions are generated by running node scripts/generate-worker-types.mjs via the types:generate script; the output file worker-configuration.d.ts is auto-generated by wrangler types and must not be edited manually.[5][6] To replace a format blueprint use pnpm import:format-blueprint <export.gadget> <blueprintId>; to add a new one use pnpm import:format-blueprint <export.gadget> --new <name>. Never manually edit a blueprintId.[3] The frontend can optionally be bundled as static assets on the backend Worker by adding an assets block to wrangler.jsonc pointing to ../workshop-frontend/dist, with not_found_handling: single-page-application and run_worker_first for /api, /api/*, and /blueprint-screenshot/*. This block is commented out by default and is not the only deployment approach.[2] A GitHub Actions workflow (.github/workflows/contribution-policy.yml) uses scripts/contribution-policy.js to automatically close pull requests that clearly violate the contribution policy; policy rules and their test cases live in scripts/contribution-policy.js and contribution-policy.test.js respectively. The monorepo uses tsgo (TypeScript's Go-based compiler) for type checking across tsconfig.json and per-package tsconfig.app.json files in gatekeeper-context, gatekeeper-scheduler, and typed-storage. If a change passes local type checking with tsc but fails CI, the compiler difference (tsgo vs tsc) is the first diagnostic step. A preview.yml GitHub Actions workflow automatically deploys a Worker preview for each pull request; PR-scoped configuration is generated by scripts/preview/staging-config.ts and deployment is driven by scripts/preview/preview.ts. Scripts in scripts/ are written in TypeScript and run via Node's native type-stripping — no separate transpile step is needed; scripts/tsconfig.json defines the type-check boundary. Every script in scripts/ must have a co-located <script>.test.ts test file; new scripts and modifications to existing scripts alike are expected to follow this convention. scripts/pnpm-command.ts is the central utility for spawning pnpm subprocesses and handles cross-platform process invocation; it is the canonical reference for how subprocess execution works across platforms. Related entry points include scripts/run-local.ts, scripts/run-dev-server.ts, and scripts/bin-entry.ts. @gadgets/scripts is a shared workspace package that exports centralized Vite and Vitest configurations; packages including gatekeeper packages, backend-utils, configurator-ui, error-reporting, and gatekeeper-kit import their build configs from it rather than maintaining local copies. New packages must extend Vite and Vitest configurations from @gadgets/scripts rather than copying configs from existing packages. scripts/run-dev-server.ts, scripts/run-local.ts, and scripts/relay-termination.ts include process-tree management improvements for concurrent runs. scripts/vp/concurrency.ts detects the host machine's CPU count and derives the vp run parallelism limit from it, replacing a static default.
The root test script runs node --test scripts/*.test.js followed by recursive pnpm run test across all packages, so the build must succeed before tests are executed — CI's test job runs pnpm build then pnpm test.[5][7] CI triggers on pushes to main and on all pull requests; repository permissions are restricted to contents: read, and both jobs use pinned, SHA-verified versions of actions/checkout and actions/setup-node with persist-credentials: false to reduce supply-chain risk.[7]
Integration tests in packages/integration-tests use wrangler's createTestHarness() to boot workshop-backend and one or more gatekeepers as real Workers in workerd, with their checked-in wrangler.jsonc patched in memory. Tests communicate over Cap'n Web via WebSocket to /api — the same transport the browser uses.[8] workshop-backend is placed first in the workers array so that unrouted requests (e.g. /api) go to it as the primary worker.[9] startTestGatekeeperHarness() is a convenience wrapper that boots the Workshop with only the bundled fixture gatekeeper bound, using binding TEST and the fixtures/gatekeeper-test directory.[9] Harness.fetchWorker(name, ...args) dispatches a request directly to a named worker's HTTP entrypoint without host resolution, so no routes config is needed; the path must still match what the worker expects.[9] The packages/integration-tests suite runs with pnpm test as part of CI's normal test job using a fixture gatekeeper, while per-vendor consumer-repo suites run in their own CI step against real vendor gatekeepers.[8] packages/integration-tests/src/rpc-client.ts provides RPC client support for Workshop lifecycle, sharing, presence, and blueprint test scenarios. Integration test modules in packages/integration-tests/__tests__/ cover Workshop lifecycle (create/open/close), sharing flows, real-time presence, and blueprint creation and output validation, exercising the public RPC interface with a mock model.
Only outbound HTTP is stubbed in the integration test suite — nothing else is mocked. The code under test runs in a separate workerd process, which has two key consequences: vi.useFakeTimers() patches only the test process's clock and is invisible to the Worker, and time-dependent logic such as isTokenExpired()'s 30-second skew in gatekeeper-shared is evaluated entirely inside that Worker.[8] Storage persists for the harness's lifetime — no test may assume a clean slate. Tests stay independent by taking fresh identities via nextUsernames() from the toolkit, using per-test resource URLs, and relying on account labels allocated by the connect/provision helper rather than chosen by the caller.[8] server.reset() costs roughly 3 seconds per call, restarts the server (making server.url undefined and killing all open WebSocket RPC sessions), and is a teardown tool — not a between-tests storage wipe.[8] fixtures/gatekeeper-test/ is a real Worker speaking the real gatekeeper protocol whose verification outcome tests control via an HTTP control route. It is scoped to overseer logic testing, not a substitute for per-vendor coverage — see Gatekeepers for the full gatekeeper protocol details.[8]
In packages/gatekeeper-scheduler/src/scheduler.ts, ScheduleSessionImpl accepts injectable now and randomId functions via ScheduleSessionDependencies, enabling deterministic unit testing of schedule registration timing and ID generation.[10]
Sources