Worker backends
On a Cloudflare project, a TypeScript backend runs inside the project's SSP Worker. No Dockerfile, no URL, configured only from sp00ky.yml.
Worker backends are new in canary.344. They run on Cloudflare Dynamic Workers, which are an open beta. TypeScript and JavaScript only for now.
A Worker backend is a normal sp00ky backend (an OpenAPI spec, an outbox method, called with
db.run()) that runs as a Cloudflare Worker instead of a Docker container. Add a worker: block to
the backend in sp00ky.yml and spky deploy does the rest: it bundles your code, uploads it, and
the project’s SSP Worker loads it as a Cloudflare Dynamic Worker.
Nothing else to set up. The backend has no Dockerfile, no baseUrl, no wrangler config and no
Cloudflare account of its own.
How it is called
Jobs, exactly as for any backend. A job row in the outbox table makes the SSP POST the row’s
path to your backend, with the payload as the JSON body. A 2xx answer marks the job success
and stores the body in result. Anything else, a thrown error or a timeout, is a failed attempt:
it lands in errors and the job retries by max_retries and retry_strategy, then ends failed.
See Jobs.
The call never leaves Cloudflare. The SSP hands the request straight to the backend’s isolate, so
your handler sees an ordinary https request with the job’s path. With an auth block the request
also carries Authorization: Bearer <token>, as on a VM, so the same bearer check works in both
places.
Nothing is public by default. A Worker backend has no URL. The only way in is a job, unless you
list paths under expose.
expose: the public escape hatch
Some routes have to answer the browser directly, the usual one being an LLM answer streamed token by
token. List those paths under worker.expose and they are served on the project host,
https://<slug>.<domain>. Requests on those paths go straight to the backend, and a streamed
response passes through as it is produced.
- A pattern starts with
/. A trailing*makes it a prefix:/llm/*covers/llm/answerbut not/llmitself. - Everything else on the host stays the SSP’s and your web app’s.
- An exposed route is public. Check the caller yourself, for example by verifying the user’s SurrealDB token.
spky lintwarns when anexposepattern also covers a path in your OpenAPI spec, because that job route would then be public too.
Prefer jobs for everything else. A job is retried, survives a reload and shows its progress as a live row; an exposed route gives you none of that.
Example
A Hono backend with Workers AI, one Cloudflare Workflow, and one public streaming route:
Call the job routes as usual:
What the code sees
export default app. The entry’s default export handles every request, jobs and exposed routes alike. A Honoappworks as it is, and so does any object with afetch(request, env)method.- Env vars as strings. The backend’s
envfromsp00ky.yml(vault values included) arrives as plain strings on the handler’senv(in Hono,c.env). Withnodejs_compatthey are onprocess.envtoo, so code written for a VM backend runs unchanged. - The project’s database. Like a VM backend, it gets
SPKY_ENV=cloud,SPKY_DB_URL(https),SPKY_DB_WS(wss),SPKY_DB_NSandSPKY_DB_NAME. On the shared free database it also getsSPKY_DB_USERandSPKY_DB_PASS, the project’s namespace owner, withSPKY_DB_LEVEL=namespace: sign in with{ namespace: SPKY_DB_NS, username, password }. On your own database, put its credentials in the vault. These names are set by sp00ky and win over your own keys. - Workers AI. With
ai: AI,env.AI.run(model, input)runs a Workers AI model.run()is the only method forwarded. - Workflows. Each
workflowsentry givesenv.<binding>, a Cloudflare Workflow binding (env.INGEST.create({ params })). Theclassmust be exported from the entry. Itsthis.envis the same env. For workflows that also run on a VM and underspky dev, with no Cloudflare Workflows behind them, use durable workflows (the same API). - Text modules. Files matched by
textModulesimport as strings (import query from './material.surql'). - WebAssembly. A
.wasmimport gives a compiledWebAssembly.Module, as in wrangler:spky deployships each imported file beside the bundle, since a Worker may not compile wasm from bytes. Import the file by its path, relative or inside a package (import wasm from 'loro-crdt/web/loro_wasm_bg.wasm'), and instantiate it yourself (initSync({ module: wasm })for a wasm-bindgenwebbuild). It counts towards the 10 MB of the bundle, as base64. - Outbound
fetchis open. Call any API you like. - No other Cloudflare bindings. No R2, KV, D1, Durable Objects or service bindings.
worker keys
| Prop | Type | Default | Description |
|---|---|---|---|
| entry | path | (required) | The entry module, relative to sp00ky.yml. Its default export handles requests. |
| compatibilityDate | YYYY-MM-DD | 2026-10-01 | Workers compatibility date. |
| compatibilityFlags | string[] | [] | Workers compatibility flags, e.g. [nodejs_compat] for Node built-ins. |
| textModules | glob[] | [] | Files imported as strings, e.g. ["**/*.surql"]. Only the extension counts: every imported .surql file becomes a string. |
| cpuMs | number | (Cloudflare's default) | CPU time per request in ms, 1 to 300000. The SSP Worker takes the largest value of its backends. |
| ai | string | - | Name of the env binding for Workers AI, e.g. AI. |
| workflows | { binding, class }[] | [] | Cloudflare Workflows the backend starts: env.<binding> runs the exported class. |
| expose | string[] | [] | Paths served publicly on the project host. A trailing * matches a prefix. Default: none. |
The rest of the backend entry is the usual: spec and an outbox method with a table are
required, auth and env work as on any backend. Binding names (ai and each workflow binding)
must differ, and the backend’s name must be lowercase letters, digits, - or _. These are
refused with worker:
hosting: external: sp00ky hosts a Worker backend.runOn: it runs inside the SSP Worker, not on a machine.
On Cloudflare, jobs go to worker://<name>, so baseUrl only matters to spky dev, which sends
jobs there locally. deploy.timeout and deploy.timeoutOverridable apply as on any backend;
deploy.dockerfile, deploy.port and deploy.resources are not used.
On a VM project
The worker block only means something on a Cloudflare project. On a VM project it is ignored and
the backend deploys from its Dockerfile, as before, with a warning from spky deploy. One
sp00ky.yml can carry both: worker for Cloudflare, deploy.dockerfile for a VM.
Local development
spky dev does not run Worker backends under workerd yet. Run the backend locally as you do today,
in Node behind a dev command, and give it a baseUrl so the local SSP can reach it:
Locally, env comes from process.env and there is no AI or Workflow binding, so keep those calls
behind a check or a small stub.
Deploying
spky deploy on a Cloudflare project, for each Worker backend:
- Bundles
entrywith esbuild into one ES module (platformneutral, run from the nearest directory aboveentrywith apackage.json, so install your dependencies first).node:*andcloudflare:*imports stay external; withnodejs_compatso do bare Node built-ins such asbuffer. This needs Node.js withnpxon the machine that deploys. - Uploads the bundle. Its hash names the version, so a deploy never reuses old code.
- Sends the backend’s settings and env. The control plane ships the bundle with the project’s
SSP Worker and creates the Workflows and the
exposeroutes. Routes you no longer list are removed.
This needs an SSP from canary.344 on. A project whose version
pins an older SSP deploys without its Worker backends.
An import esbuild cannot resolve fails the deploy and names the module. For a Node built-in, import
it as node:<name> and add nodejs_compat. --only api deploys just that backend and keeps the
others as they are; a devOnly backend is never bundled. See Deploying.
Limits
A job request times out after deploy.timeout seconds, 10 when unset. Set it for slow routes
such as an LLM call (deploy: { timeout: 120 }), or have the job route start a Workflow and
return.
- Cold starts. Loading a backend and answering its first call takes 29 ms (1 KB bundle) to 350 ms (1.3 MB). Warm calls cost under 1 ms. Idle isolates go away within a few minutes, so a quiet project pays the cold load on most jobs. Keep bundles small.
- At least once. A job in flight when the SSP Worker is evicted runs again, whatever its
max_retries. Make job routes safe to repeat, for example with a key from the payload. - TypeScript and JavaScript only. Rust, Python and Go Worker backends are planned.
- Cloudflare projects only. Containers (Docker backends) do not run on Cloudflare yet.