celld-operator

Cloudflare compatibility

What runs, what is planned, and what is out of scope. The governing rule, inherited from celld and mirrored by the platform: a gap fails loudly, at deploy time or first use, instead of silently. A silent gap is a bug.

celld's scope rule: if Cloudflare builds a function on Durable Objects, celld can get that function; a function built on a different primitive is out of scope. That is why D1 is planned (a D1 database is a Durable Object with a SQL API) while KV is not (a different consistency model).

Service matrix

StatusServices
SupportedModule Workers (fetch, JS RPC, service bindings, vars) · Durable Objects (SQLite storage, alarms, hibernatable inbound WebSockets, outbound WebSocket clients, one writer per cell, names as addresses, RPC on stubs) · Static assets (assets.directory, binding, html_handling, not_found_handling, run_worker_first, _headers, _redirects; asset-only projects deploy without a Worker) · WebAssembly · a substantial node: subset · Worker Loader / Code Mode (experimental)
Planned upstreamD1 · Workflows · Queues (if demand appears). All are built on Durable Objects
Not plannedKV · R2 (declared r2_buckets bindings load, but every method throws) · Cache API · cron scheduled handlers · Workers AI (an experimental HTTP adapter exists behind CELLD_AI_URL) · Vectorize · Hyperdrive · Browser Rendering · Email · custom domains · TLS termination

Runtime API status

APIStatus on celld
Fetch / Request / Response / HeadersYes. Gaps: Response.redirect(), Response.error(), the cache request option.
Handlersfetch, alarm, webSocketMessage/Close/Error, RPC. No scheduled (cron), queue, tail, or email.
JS RPCYes: WorkerEntrypoint, RpcTarget, promise pipelining, stubs in DO storage. Limit: a stub cannot cross an isolate boundary yet, and a cross-isolate service binding with a named entrypoint supports single method calls only.
StreamsYes, including byte streams, BYOB, CompressionStream/DecompressionStream. Gap: ReadableStream.from().
WebSocketsYes: inbound hibernatable sockets with attachments, outbound clients, setWebSocketAutoResponse (answers without waking the cell). Gap: getTags().
Web CryptoPartial: digest, HMAC sign/verify, AES-GCM/CBC/CTR, Ed25519 and ECDSA P-256 signing, RSA-OAEP decrypt (not encrypt), and most import/export paths, plus Cloudflare's timingSafeEqual and DigestStream. Missing: wrapKey/unwrapKey, RSA-PSS signing, HKDF/PBKDF2 via deriveBits (both available through node:crypto). An unavailable algorithm throws.
Web standardsURL, URLPattern, AbortController/AbortSignal, Blob/File/FormData, structuredClone, queueMicrotask, encodings, atob/btoa. Caveats: a signal does not abort across an RPC call; signal.onabort is accepted but never invoked (use addEventListener('abort')); structuredClone is not conformant on exotic types.
WebAssemblyYes: V8's own, without restrictions. .wasm imports yield compiled modules (same rule as Wrangler), compiled once per process. Rust via workers-rs works.
TimerssetTimeout, setImmediate, scheduler.wait(). setInterval throws. performance.now() has millisecond resolution.
node: compatAlways available: celld externalizes node:* at bundle time and does not need (or read) the nodejs_compat flag. Implemented: assert, async_hooks (real AsyncLocalStorage), buffer, events, path, stream, timers/promises, util. Partial: crypto, zlib (sync gzip/deflate only), fs (reads fail with ENOENT). Not implemented: http(s), net, tls, dns, os, process (the process global exists; the module does not), vm, worker_threads, child_process.
Context (ctx)waitUntil, props, exports. passThroughOnException() is accepted but has no effect — there is no CDN behind it. ctx.facets is absent.
Consolelog/info/warn/error are real. debug/trace/group/table do nothing; assert/time/count are absent.
EventSource, MessageChannel, BroadcastChannelNo. The classes exist so bundles load, but they do nothing.
Cache (caches), HTMLRewriter, TCP sockets, facetsNo.

Known silent gaps. celld's rule is that an unavailable API fails loudly, and most do — an unknown Wrangler key fails the deploy, an unavailable Web Crypto algorithm throws, setInterval throws. These are the upstream-documented exceptions, which accept your call and quietly do nothing:

  • cloudflare:sockets' connect() returns an inert stub instead of throwing
  • unimplemented node: modules import as inert stubs
  • EventSource, MessageChannel, BroadcastChannel construct but do nothing
  • ctx.passThroughOnException() is accepted with no effect
  • signal.onabort is accepted but never invoked
  • console.debug/trace/group/table do nothing
  • a compatibility flag celld does not model is accepted without effect (and is absent from Cloudflare.compatibilityFlags rather than reported as enabled)

Wrangler configuration

celld deploy accepts wrangler.jsonc or wrangler.json (not wrangler.toml) with a strict key allowlist:

name · main · compatibility_date · compatibility_flags · durable_objects · migrations · assets · services · vars

Any other key (routes, kv_namespaces, triggers, …) stops the deploy with an error naming the key. Run celld deploy . --dry-run in CI so unsupported configuration fails on the pull request, never at runtime.

Honored compatibility flags: js_rpc, delete_all_deletes_alarm, fetcher_no_get_put_delete, websocket_standard_binary_type, plus assets navigation behavior. An unmodeled flag is accepted without effect and reported absent from Cloudflare.compatibilityFlags.

Working around the gaps

Neither celld nor the operator ships emulated bindings: a missing service stays missing rather than becoming a fake that behaves differently under load. Today the gaps are closed in application code, not by the platform:

Entity, not process

Durable-execution engines (Temporal, Restate, Azure Durable Functions) model a process: a workflow that runs to completion. A cell models an entity: a user, a room, a document, an agent. It is a small stateful server that lives as long as its name does. If your domain decomposes into named entities, cells shard it by construction.