Blog ·

The control plane is just another worker

With 0.15, the OpenWorkers API and dashboard run as a worker on OpenWorkers. What dogfooding-only changes, and what it deliberately forbids.

As of 0.15, the REST API and the dashboard that manage OpenWorkers are served by OpenWorkers. The control plane is a worker, deployed with the same CLI command any user runs, subject to the same limits, paying the same latency budget. There is no standalone API server anymore - not in production, not in the docs. The only non-worker pieces left are the runner itself, the scheduler, and the database they share.

What serves your requests now

The API is one TypeScript app that answers both api. and dash. hostnames: REST routes on one side, the dashboard’s static assets on the other, served through its assets binding. It talks to Postgres through the database binding - the same D1-shaped interface every worker gets. Its row in the workers table looks like anyone else’s.

The proxy in front has no special case for it. Every hostname takes the same path: nginx forwards to the runner, the runner resolves the worker from the request, executes it, returns the response. The upstream blocks that used to special-case the API and the dashboard are deleted.

Custom domains, resolved by a worker

Routing a custom domain needs an answer to “whose domain is this?” That lookup is an API endpoint - which means nginx asks a worker how to route to workers. The proxy sends an auth subrequest to /api/domain/$host, the API worker answers with a Worker-Id header, nginx caches it for 30 seconds and forwards. The recursion terminates because the API’s own hostnames are static: the runner resolves those directly from the database, no subrequest involved.

Why run it this way

The platform team now lives on the product. If cold starts hurt, we feel them on every dashboard load before any user writes in. If the API needs streaming responses, the runtime has to support streaming responses - features stop being demo-grade the moment the control plane depends on them. It also removes an entire always-on service from operations: no API deployment pipeline, no API host, just a worker version to roll forward or back.

What it deliberately forbids

A control plane that runs on the platform cannot repair the platform. If the runner is down, the API is down with it - so nothing infrastructural is allowed to depend on the API. Migrations, setup and recovery go through the CLI, which talks to Postgres directly. The database is the single source of truth: the runner and the scheduler read it without the API in the path, so the control plane going away degrades administration, never execution.

Also in 0.15

The release that carries the switch also ships typed byte parameters and results in the SQL binding - blobs cross the boundary as bytes, not strings.

The next step is already on branches: one runner binary serving both worker species side by side, JavaScript in V8 and Rust as native WASM components. That story - and why porting a Cloudflare workers-rs app to it took a Cargo rename and three deletions - is in Running workers-rs without JavaScript.