Working on the Cloudflare package

Develop and test the Cloudflare deployment in packages/cloudflare, an optional package in the npm workspace. It uses Wrangler to run the Worker and Vitest to test it in the workerd runtime. You can work on core, the CLI or website without running the Cloudflare tools; use them when your change affects this deployment.

The request path

The Worker receives the public request. For paths outside /api, /auth, /files, /healthz and /schema, GET and HEAD normally load the website from the Worker's static assets (the Vite build, src/website.ts) with the server's security headers. /wake says whether the server is ready, and starts it if needed. The website and CLI poll this route to show startup progress. Anonymous cacheable reads can end at the Worker, and published files (/files/…) are served straight from D1 metadata and R2 bytes without starting the container. Other requests go to the Quaso container, which runs the same server image as Docker Compose. The server owns the service logic: accounts, permission checks, translations, LLM jobs and the nightly R2 export. It reaches storage through two private outbound hosts that the container controller registers: d1.quaso.internal (src/d1_handler.ts, SQL batches against the DB binding) and r2.quaso.internal (src/r2_handler.ts, objects in the BACKUPS bucket). The public Worker returns 404 for the private API paths.

QuasoContainer (src/container.ts) is the container controller, a Durable Object that holds no project data. It starts the server once for everyone who waits (/wake and requests alike), and remembers how long the last start took for the progress bar. Before letting the container sleep it asks the server's /healthz whether it is busy and when its nextWakeUp is, and schedules a wake-up for it: LLM jobs and the nightly export depend on that, so preserve both when changing scheduling. Enter the Gemini key in Settings; D1 stores it with the other project settings. The container keeps no lasting project data on its disk.

The controller also stores the Worker deployment ID when it starts the server. After a deployment, it stops an older server on a request with no other requests in progress, but only if /healthz reports busy: false. It waits for the stop before starting with the deployed image and settings. Keep this check when changing startup: continuous use must not leave the old server running indefinitely, and a restart must not cut off an LLM job or export.

Run locally

From the repository root, with Docker running:

npm ci
cp packages/cloudflare/.dev.vars.example packages/cloudflare/.dev.vars

Set SECRET_KEY to the output of openssl rand -hex 32. Also uncomment SETUP_KEY and set it to the output of openssl rand -hex 16; the server needs it to allow first setup. Enter a Gemini key in Settings after setup if you want real translation. Then run:

npm run cf:dev

Open http://localhost:8787/setup and create the first administrator with your setup key. Wrangler builds the container from deploy/Dockerfile; the build time depends on the host and image cache. D1 and R2 are Wrangler's local simulations, kept in its ignored state directory. D1 Time Travel needs a deployed database and is unavailable locally.

The top level of wrangler.jsonc is for local work. The deployment tasks select a named environment: cf:setup and cf:deploy generate a temporary configuration from the staging or production template and the operator's quaso.cloudflare.jsonc (scripts/instance_config.ts), cf:deploy requires --env; cf:setup asks for the environment if it is missing. See deployment for accounts, domains and secrets.

Types, fixtures and tests

npm run cf:types
npm run cf:fixtures
npm run cf:check

After changing wrangler.jsonc, commit regenerated worker-configuration.d.ts. After changing shared behavior, review and commit the fixture from packages/cloudflare/scripts/fixtures.ts. cf:check checks both generated outputs, compiles the package, runs its tests in workerd, and bundles the Worker without deploying it. Tests compare the same service scenario with local SQLite.

Keep service and core portable: no runtime-specific imports, such as node:, in their production code. Worker modules may export only supported handlers and classes. The D1 adapter (packages/service/src/adapters/d1_sql.ts) sends each commit as one batch, guarded by the database revision. Use the asynchronous Sql.read, Sql.commit and Sql.migrate methods and the retry helper in packages/service/src/write.ts. The older SyncSql.transaction API is for local synchronous callers; it is not the D1 write path.

Rollout compatibility

A Worker becomes active before all containers update. The private D1 and R2 handlers must accept the previous server release during this overlap, and read retries must remain safe. Retry a failed write only when the write protocol proves that doing so is safe. Schema migrations run when the server starts. Test restored and upgraded databases as well as brand-new ones. See operations for recovery.

View this page on GitHub