CellB
Isomorphic Durable Objects · celld-compatible

One cell model.
Browser, server, cloud.

CellB runs Workers and Durable Objects unchanged in a browser tab, in Node, and alongside celld on one shared bucket. It is an architecture for rich, isomorphic applications built for people and for AI agents.

npm install @statewalker/cellb
fleet · s3://cells3 nodes live

browser tab

CellB · SQLite WASM on OPFS

doc:spec
agent:review

node-a

CellB · node:sqlite

room:lobby
user:ada

edge-1

celld · V8

queue:jobs
kv:sessions

cells/Agent:review/

own.json { node: "tab-7f", epoch: 3 }

ltx/e1/… celld · ltx/e2/… node-a · ltx/e3/… browser

// the architecture

Cells are the unit of the application. Placement is a deployment detail.

A cell is a named object with its own code, its own SQLite database and exactly one owner at a time. Build an application from cells, such as users, documents, rooms, workspaces and agents, and the same program can run entirely in a tab, entirely on servers, or across both, with each cell running where it is needed.

Isomorphic

A Worker module and its Durable Object classes run unmodified in a browser tab, in Node, and on celld. Where a cell runs is a deployment choice, not a rewrite.

Durable

Each cell owns a synchronous SQLite database, with input and output gates and an implicit transaction per turn. With an LTX log, every acknowledged write survives a crash.

Movable

CellB stores cells exactly as celld does. One cell can be served by celld, then by CellB in Node, then by a browser, through one shared bucket and with no conversion.

// where it runs

One core, three hosts.

The runtime core knows nothing about its platform. Ports supply SQLite, storage, leases and clocks; the package has one entry point per platform.

In the browser

@statewalker/cellb/browser
  • SQLite WASM in Dedicated Workers: on OPFS pools, on LTX files in OPFS read page by page, or in memory with an IndexedDB log
  • Requests through a ServiceWorker, even at the origin's root; WebSockets over MessagePorts
  • Shards spread across the open tabs of an origin; another tab takes over when one closes
  • A whole app served from static files keeps running when the file server stops

On the server

@statewalker/cellb/node
  • node:sqlite, one file per cell
  • loadNodeProject runs a wrangler project as celld deploys it
  • Loaded Workers in worker threads of their own; bundles from other bundlers through host-modules
  • Speaks celld's peer protocol to reach owners on other nodes

In the cloud, with celld

celldBucket(…)
  • One S3, GCS or Azure bucket shared by celld nodes, CellB nodes and browsers
  • celld's ownership records, node leases, epochs and wake index
  • Exactly one owner per cell, with self-fencing on a lapsed lease
  • A cell that CellB wrote, celld continues, and back

// use cases

Built for rich applications, people and agents.

IDE-class tools, agent platforms and collaborative apps all need the same thing: many small stateful services that run close to whoever uses them, human or machine.

IDE-class applications

Editors like VS Code and Theia split into a UI, workspace services and extension hosts that run locally or remotely behind one API. With CellB each of those parts is a cell: documents, workspaces, language services. Plugins run as loaded Workers in a realm of their own. The same plugin runs beside the UI in a tab or on a server, and the code stays the same.

AI agents as Durable Objects

An agent or conversation is a cell whose transcript lives in its own SQLite. Alarms drive long runs, SSE and WebSockets stream replies, and env.AI reaches any OpenAI-compatible model: llama.cpp, Ollama or a hosted API. Flue, pi and opencode already run on CellB. If a node stops during a model call, another node on the same bucket takes the cell and finishes the submission.

Isolated code that agents write

Worker Loader bindings run code handed over at run time, each in its own worker thread, or in a browser its own Dedicated Worker. The loaded code gets its own globals and only the env you give it. Set globalOutbound: null to block its fetch(), or route its fetches through a Fetcher you control. It is not a sandbox for hostile code: see Security & isolation.

Local-first apps

Cells answer from OPFS inside the tab, with no network and no server: EmDash keeps publishing posts after its file server is stopped. Configure a fleet bucket and the same cells are logged in celld's format, so a server node can serve them when the tab is closed.

Apps for people and for agents

A cell is an addressable endpoint with fetch, RPC and WebSockets. A person's UI and an agent's tool calls reach the same state through the same object, with one owner and one transaction model.

Workers without native binaries

Run a wrangler project inside vitest, Node or Chromium, with no workerd or celld binary. Tests of agent apps run offline against @statewalker/fake-openai.

// real apps

Large Cloudflare apps that CellB did not write, running in a tab.

The examples use CellB as its users do, from npm, and a nightly CI run checks them on CellB as the next release would publish it. Running them found gaps in CellB, which are now closed.

EmDash's post editor, running in a browser tab on CellB, after publishing a post with a featured image

A CMS in a tab, from static files

EmDash, the Astro CMS, runs unchanged from its Cloudflare build: D1, KV sessions, R2 media, a cron trigger, passkeys. A plain file server hands the browser the files and is then stopped. The setup wizard, sign-in, editor and media library all keep working, and the post is still there after the tab is reopened.

How it is wired
The host tab's build log: EmDash's 225 routes built in the browser in about 73 seconds

Built from source, in the same tab

Astro's compiler and esbuild run as WebAssembly in workers, with packages from the npm registry kept in a package store that is itself a Durable Object. EmDash's 225 routes build in about 65–115 s on 4 CPUs and run on CellB at once, with their cells on LTX files in OPFS.

How it is wired

// the code

Workers code you already have.

A runtime is a plain (Request) ⇒ Responsehandler. Mount it on node:http, a ServiceWorker or a test. Wrangler projects load from the configuration celld deploys, and anything CellB does not support is listed in unsupportedinstead of being silently ignored.

  • Synchronous ctx.storage.sql, as on Cloudflare
  • Stubs with fetch, RPC and promise pipelining
  • Alarms, WebSockets, hibernation, facets
counter.ts
import { createCellRuntime, DurableObject } from "@statewalker/cellb";
import { nodeSqliteOpener } from "@statewalker/cellb/node";

class Counter extends DurableObject {
  increment(): number {
    const sql = this.ctx.storage.sql;
    sql.exec("CREATE TABLE IF NOT EXISTS c (id INTEGER PRIMARY KEY, n INTEGER)");
    sql.exec("INSERT INTO c (id, n) VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET n = n + 1");
    return sql.exec<{ n: number }>("SELECT n FROM c WHERE id = 1").one().n;
  }
}

const runtime = createCellRuntime({
  worker: {
    Counter,
    default: {
      fetch: async (req: Request, env: any) =>
        Response.json({ n: await env.COUNTER.getByName(new URL(req.url).pathname).increment() }),
    },
  },
  durableObjects: { COUNTER: { className: "Counter" } },
  openSql: nodeSqliteOpener("./data"),
});

await runtime.fetch(new Request("http://local/counter/a")); // {"n":1}

// next to celld

celld is the judge.

celld's examples, CellB's own probe projects and workerd's Durable Object tests run on the real celld and are recorded. The recordings are then replayed against CellB in Node and in Chromium on every pull request. That is 70 scenarios: 5 are judged by workerd instead, and 6 are known differences.

70

recorded scenarios

2

runtimes replayed: Node, Chromium

5

judged by workerd

6

known differences

Featurecelld 0.6.3CellB · NodeCellB · browsersHow CellB is checked
Workers (stateless handlers)supportedyesyeshello, webapi, router, body
Durable Objects / cellssupportedyes, same storage formatyes, same storage formatcounter, rpc, alarm, workerd's tests; cells cross celld ↔ CellB
Durable Object facetssupportedyesyesfacets, facets-api
KVsupportedyes (celld's code)yeskv, kv-values
Queuessupportedyes (celld's code)yesqueues, queues-api
D1supportedyes (Miniflare's simulator)yesd1, D1 migrations
R2supportedyes, celld's bucket layoutyesr2, r2-api, r2-advanced
Workflowssupportedyes (celld's engine)yesworkflow, workflow-api
Cron triggerssupportedyesyescron
Static assetssupportedyes (Cloudflare's behaviour where celld has bugs)yesstatic-assets, assets-api
Dynamic Workers (Worker Loader)supportedyes, a worker thread eachyes, a Dedicated Worker eachdynamic-worker-tails, Worker Loader scenarios
WebSockets, hibernationsupportedyesyes, over MessagePortswsecho, wsclient, wd-websocket-* (CellB passes wd-websocket-hibernation)
Vectorize-style searchexampleyesyesvectordb
Workers AI (ai binding)no bindingyes, on any OpenAI-compatible endpointyesp14-workers-ai, Flue's cloud agent
Agent frameworks: Flue, pi, opencodeexamples (pi, opencode need the network)yes, offline against a fake modelFlueexamples/flue, examples/agents
Python Workers (Pyodide)supportedno, out of scopenonot run
Containersexperimental (Docker or Podman)no; reported in unsupportednonot run
supportedpartial, example or differentnot supportedWhere CellB follows Cloudflare instead of celld

// life of a cell

One writer, and no lost writes.

A cell is safe when only one runtime writes it at a time and every acknowledged write can be restored somewhere else. On a fleet bucket, CellB uses celld's own ownership protocol to guarantee both.

  1. 01

    Take

    The first activation creates own.json with a conditional write. Later activations raise the epoch with compare-and-swap.

  2. 02

    Run

    Input and output gates serialize events. The first SQL statement opens a transaction that commits at the end of the turn.

  3. 03

    Log

    Each commit is written as LTX, celld's format. The first commit of a generation is a snapshot. A reply leaves only once its write is durable.

  4. 04

    Move

    Closing releases the record. The next owner, whether celld, Node or a tab, restores the newest generation and continues.

// by the numbers

Measured, and reproducible.

The rows celld.dev reports, measured for CellB on Node and in Chromium by the benchmark in the repository. The page reads its last recorded run.

NodeChromium
Durability
Writers per cell (epoch-fenced)11
Acknowledged writes lost on SIGKILL (of 300)0RPO=0—
Durable write latency (bucket 90 ms away)2~92ms—
CellB's own cost per durable write20.88ms—
Commit to OPFS (local)—8.8ms
Failover after node loss, 0 lost3~9s—
Speed (warm)1
Stateless request p50 / p99, in process40.01 / 0.04ms≤0.10ms
Stateless request p50 / p99, loopback HTTP0.13 / 0.51ms—
Stateless throughput / thread, in process~80kreq/s~66kreq/s
Stateless throughput, loopback HTTP~18kreq/s—
Wake a hibernated cell~0.21ms~1.6ms
Wake a cell whose database was closed~1.8ms—
Activate an inactive cell from the bucket2~4.9ms—
Density & cost1
RAM per resident cell50.16MB0.25MB
Resident cells / 8 GB node~41,000cells—
Inactive cell cost~0bucket ops~0bucket ops
Server $ / resident cell-month6~$0.001$0
  1. One machine, trivial cells (a table and a row each): a 4-vCPU Intel(R) Xeon(R) Processor @ 2.10GHz VM with 16 GB, Node v22.22.0, headless Chromium; measured 2026-11-02. These are CellB's own costs, not a fleet's. They are not a comparison with celld's numbers, which come from other hardware.
  2. The bucket is simulated: every store operation waits a fixed 0 or 90 ms. A real S3 bucket adds its own tail latency.
  3. A second node may take a cell only after the first node's lease expires (10 s by default). The failover is that wait plus the restore.
  4. A Dedicated Worker's clock has about 0.1 ms resolution, so sub-millisecond browser rows show an upper bound.
  5. Node: process RSS growth per cell, after GC. Chromium: SQLite WASM heap growth per cell (the JS heap is not counted).
  6. At about $50 a month for an 8 GB node, as celld's docs price one. In a browser, the cell runs on the user's own device.
Reproduce: pnpm bench andpnpm bench:browser

// try it

Tabs share one origin's cells.

In a browser, CellB places cells in shards, and each shard is owned through one Web Lock. This small demo uses the same primitive. Open the page in a second tab, press increment in both, then close the owner tab and watch the other one take over.

The four browser topologies
counter:demo
connecting…

value

0

Every tab can increment. Only the owner tab writes; the others forward a request to it. Close the owner tab and another tab takes the lock and continues from the stored value.

    Build once. Place every cell where it belongs.

    Start from a Worker you have, run it in Node or a tab, and join a celld fleet when you need servers.