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/cellbbrowser tab
CellB · SQLite WASM on OPFS
node-a
CellB · node:sqlite
edge-1
celld · V8
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.

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
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
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}// in a Dedicated Worker
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
import { browserProjectRuntime } from "@statewalker/cellb/browser";
const host = await browserProjectRuntime({
config, // the parsed wrangler.jsonc
code: { bundle }, // the project's main, bundled for browsers
sqlite3: await sqlite3InitModule(),
pool: "my-app", // the OPFS SQLite pool of this worker
filesDir: "my-app-files",
// or keep each cell as celld does, LTX files in OPFS read page by page:
// bucket: { ltx: "my-app-ltx", node: "tab" },
});
const res = await host.runtime.fetch(new Request("http://browser/"));import { celldBucket, s3ObjectStore } from "@statewalker/cellb";
import { loadNodeProject, nodeSqliteOpener } from "@statewalker/cellb/node";
// The same wrangler project celld deploys, on the bucket celld nodes use.
const { runtime, unsupported } = await loadNodeProject("./counter", {
openSql: nodeSqliteOpener(), // each cell is restored from the bucket on first use
files,
storage: celldBucket(s3ObjectStore({ endpoint, bucket, region, accessKeyId, secretAccessKey }), {
node: "cellb-1",
}),
});
await runtime.fetch(new Request("http://localhost/increment?name=a"));
await runtime.close(); // releases the cells, so celld can take them at once// 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
| Feature | celld 0.6.3 | CellB · Node | CellB · browsers | How CellB is checked |
|---|---|---|---|---|
| Workers (stateless handlers) | supported | yes | yes | hello, webapi, router, body |
| Durable Objects / cells | supported | yes, same storage format | yes, same storage format | counter, rpc, alarm, workerd's tests; cells cross celld ↔ CellB |
| Durable Object facets | supported | yes | yes | facets, facets-api |
| KV | supported | yes (celld's code) | yes | kv, kv-values |
| Queues | supported | yes (celld's code) | yes | queues, queues-api |
| D1 | supported | yes (Miniflare's simulator) | yes | d1, D1 migrations |
| R2 | supported | yes, celld's bucket layout | yes | r2, r2-api, r2-advanced |
| Workflows | supported | yes (celld's engine) | yes | workflow, workflow-api |
| Cron triggers | supported | yes | yes | cron |
| Static assets | supported | yes (Cloudflare's behaviour where celld has bugs) | yes | static-assets, assets-api |
| Dynamic Workers (Worker Loader) | supported | yes, a worker thread each | yes, a Dedicated Worker each | dynamic-worker-tails, Worker Loader scenarios |
| WebSockets, hibernation | supported | yes | yes, over MessagePorts | wsecho, wsclient, wd-websocket-* (CellB passes wd-websocket-hibernation) |
| Vectorize-style search | example | yes | yes | vectordb |
| Workers AI (ai binding) | no binding | yes, on any OpenAI-compatible endpoint | yes | p14-workers-ai, Flue's cloud agent |
| Agent frameworks: Flue, pi, opencode | examples (pi, opencode need the network) | yes, offline against a fake model | Flue | examples/flue, examples/agents |
| Python Workers (Pyodide) | supported | no, out of scope | no | not run |
| Containers | experimental (Docker or Podman) | no; reported in unsupported | no | not run |
// 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.
- 01
Take
The first activation creates own.json with a conditional write. Later activations raise the epoch with compare-and-swap.
- 02
Run
Input and output gates serialize events. The first SQL statement opens a transaction that commits at the end of the turn.
- 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.
- 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.
| Node | Chromium | ||
|---|---|---|---|
| Durability | |||
| Writers per cell (epoch-fenced) | 1 | 1 | |
| Acknowledged writes lost on SIGKILL (of 300) | 0RPO=0 | — | RPO=0 |
| Durable write latency (bucket 90 ms away)2 | ~92ms | — | ms |
| CellB's own cost per durable write2 | 0.88ms | — | ms |
| Commit to OPFS (local) | — | 8.8ms | ms |
| Failover after node loss, 0 lost3 | ~9s | — | s |
| Speed (warm)1 | |||
| Stateless request p50 / p99, in process4 | 0.01 / 0.04ms | ≤0.10ms | ms |
| Stateless request p50 / p99, loopback HTTP | 0.13 / 0.51ms | — | ms |
| Stateless throughput / thread, in process | ~80kreq/s | ~66kreq/s | req/s |
| Stateless throughput, loopback HTTP | ~18kreq/s | — | req/s |
| Wake a hibernated cell | ~0.21ms | ~1.6ms | ms |
| Wake a cell whose database was closed | ~1.8ms | — | ms |
| Activate an inactive cell from the bucket2 | ~4.9ms | — | ms |
| Density & cost1 | |||
| RAM per resident cell5 | 0.16MB | 0.25MB | MB |
| Resident cells / 8 GB node | ~41,000cells | — | cells |
| Inactive cell cost | ~0bucket ops | ~0bucket ops | bucket ops |
| Server $ / resident cell-month6 | ~$0.001 | $0 | |
- 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.
- The bucket is simulated: every store operation waits a fixed 0 or 90 ms. A real S3 bucket adds its own tail latency.
- 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.
- A Dedicated Worker's clock has about 0.1 ms resolution, so sub-millisecond browser rows show an upper bound.
- Node: process RSS growth per cell, after GC. Chromium: SQLite WASM heap growth per cell (the JS heap is not counted).
- 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.
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 topologiesvalue
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.