How Celld Implements Durable Object Storage with Per-Cell SQLite Databases

Celld isolates each Durable Object (called a cell) in its own SQLite database file, using WAL mode for durability, a custom SQL authorizer for security, and savepoint-based transactions to implement the full Durable Object storage API.

Celld, the open-source Durable Object runtime from Deno, provides every cell with an independent SQLite database to guarantee strict data isolation and ACID semantics. This architecture implements the Cloudflare Workers Durable Object storage contract while maintaining a lightweight, single-process runtime. Understanding how Celld maps Durable Object storage to per-cell SQLite files reveals the engineering decisions behind its transactional guarantees and security model.

The Rust Storage Layer (crates/celld/storage.rs)

The core storage implementation lives in the Rust codebase, where each cell receives a dedicated database connection managed through a thread-local registry.

Per-Cell Database Initialization

Celld maintains a thread-local HashMap<String, Connection> named DBS that maps a cell’s unique scope (its ID) to a live rusqlite::Connection (lines 19‑22). When a cell activates, the open(scope, path) function creates or re-opens the SQLite file for that specific scope and invokes schema(&c) to initialize three internal tables:

  • kv – The key-value store (CREATE TABLE IF NOT EXISTS kv (scope TEXT, k TEXT, v TEXT, PRIMARY KEY(scope,k)) – lines 24‑27)
  • alarms – Persisted alarm data (lines 29‑33)
  • cell_metadata – Optional cell-level metadata (lines 36‑38)

The connection is configured for WAL (Write-Ahead Logging) journal mode and NORMAL synchronous setting, which provides fast commits while guaranteeing durability on process crash (lines 14‑22).

Security Hardening and Resource Limits

To protect the runtime from resource exhaustion and privilege escalation, Celld implements several security controls:

  • Resource caps: SQLite limits for SQL length, column count, and other parameters are capped via sqlite3_limit (lines 64‑80)
  • SQL authorizer: The authorize_sql function denies privileged actions such as ATTACH, DETACH, and extension loading. It also blocks access to internal tables prefixed with _cf_ using the is_reserved_sql_name helper (lines 15‑43 and lines 60‑63)

ACID Transactions via SQLite Savepoints

The transaction_control function implements the Durable Object transaction model using SQLite savepoints. When JavaScript initiates a transaction:

  • The start action creates a savepoint using BEGIN IMMEDIATE for the outermost transaction or SAVEPOINT … for nested ones
  • The commit and rollback actions translate to COMMIT, RELEASE, or ROLLBACK TO … as appropriate (lines 154‑168)

Before executing any operation, require_sql_healthy checks a per-cell "critical-error" flag that is set whenever SQLite reports a fatal error, ensuring that a corrupted database aborts further operations rather than propagating corruption.

SQL Execution and Cursor Management

Celld exposes the ctx.storage.sql API through several Rust functions:

  • sql_exec: Runs a full batch when the query contains multiple statements and no bind parameters; otherwise prepares a statement, binds JSON values, and returns column names, rows, and a write-position delta (lines 115‑142)
  • Cursor API: sql_cursor_start, sql_cursor_next, and sql_cursor_close enable iterating large result sets without loading everything into memory. The implementation reuses prepared statements via an LRU cache (SQL_STATEMENT_CACHES) to avoid re-compilation overhead (lines 300‑375)

When a cell hibernates, the close(scope) function removes the connection and all per-cell caches from DBS, allowing the replicator to release the file (lines 90‑99).

The JavaScript Harness (crates/celld/js/harness.js)

The JavaScript layer provides the developer-facing DurableObjectStorage API by forwarding calls to the native Rust functions.

Bridging to Native Storage

The DurableObjectStorage class serves as the JavaScript façade. Its constructor stores the cell’s scope and initializes a SqlStorage helper (this.sql = new SqlStorage(scope)) (lines 42‑55). All storage operations invoke native functions prefixed with __storage_* (e.g., __storage_get, __storage_put, __alarm_set).

KV Operations and Consistency

Key-value methods (get, put, delete, list) call their corresponding native functions after flushing any pending puts via _flushPendingPuts. This synchronization keeps the synchronous Rust side consistent with the async JavaScript contract (lines 69‑84, lines 81‑92, lines 94‑106). Alarm methods (setAlarm, getAlarm, deleteAlarm) map directly to __alarm_* helpers (lines 42‑57).

Transaction Wrappers

Transaction support in JavaScript builds on the Rust transaction API:

  • _transactionStart calls __storage_transaction_control(..., "start", …)
  • _transactionCommit and _transactionRollback invoke the same native function with "commit" or "rollback" actions (lines 58‑71)

The high-level transactionSync method (and its async variant) exposes the Durable Object pattern of "run a callback inside a transaction," automatically committing on success or rolling back on error (lines 98‑113).

Practical Code Examples

The following examples demonstrate how the per-cell SQLite storage works in practice.

Basic KV Operations

export class Counter extends DurableObject {
  async fetch(request) {
    const storage = await this.state.storage;
    await storage.put("hits", (await storage.get("hits")) + 1);
    const hits = await storage.get("hits");
    return new Response(`Hits: ${hits}`);
  }
}

Raw SQL Access

export class Counter extends DurableObject {
  async fetch(request) {
    const storage = await this.state.storage;
    // Create a table only the first time this cell runs
    await storage.sql.exec(`
      CREATE TABLE IF NOT EXISTS clicks (ts INTEGER PRIMARY KEY, note TEXT);
    `);
    // Insert a row
    await storage.sql.exec(
      `INSERT INTO clicks (ts, note) VALUES (?, ?)`,
      [Date.now(), "first click"]
    );
    // Query the table
    const { rows } = await storage.sql.exec(`SELECT * FROM clicks`);
    return new Response(JSON.stringify(rows));
  }
}

Transaction with Automatic Rollback

export class Counter extends DurableObject {
  async fetch(request) {
    const storage = await this.state.storage;
    return storage.transactionSync((tx) => {
      // All writes are part of the same SQLite savepoint
      tx.put("a", "1");
      tx.put("b", "2");
      // Throwing aborts the transaction
      if (Math.random() < 0.5) throw new Error("boom");
      return "committed";
    });
  }
}

Summary

  • Per-cell isolation: Each Durable Object receives its own SQLite file identified by a unique scope, managed in a thread-local HashMap called DBS.
  • Durability configuration: Databases use WAL mode and NORMAL synchronous settings to balance performance with crash safety.
  • Security controls: A custom authorize_sql function blocks dangerous operations (ATTACH, DETACH) and reserved table names, while sqlite3_limit prevents resource exhaustion.
  • Transaction semantics: Savepoints implement nested transactions; transaction_control handles BEGIN IMMEDIATE for outer transactions and SAVEPOINT for nested ones.
  • SQL API: Prepared statements are cached in an LRU; cursor-based iteration prevents memory pressure for large result sets.
  • Lifecycle management: close(scope) releases connections and caches when cells hibernate, allowing file cleanup by the replicator.

Frequently Asked Questions

How does Celld ensure data durability with SQLite?

Celld configures each per-cell database with WAL (Write-Ahead Logging) mode and NORMAL synchronous settings. According to the source code in crates/celld/storage.rs (lines 14‑22), this combination ensures that committed transactions survive a process crash while avoiding the performance penalty of full synchronous writes. The require_sql_healthy check also prevents further operations if SQLite reports a critical error, protecting data integrity.

What security measures protect the SQLite databases?

The implementation uses a custom authorizer (authorize_sql) that denies privileged SQL operations like ATTACH, DETACH, and extension loading. It also blocks access to internal tables prefixed with _cf_ via the is_reserved_sql_name helper (lines 15‑43). Additionally, sqlite3_limit caps SQL length and column counts to prevent resource-exhaustion attacks (lines 64‑80).

How are transactions isolated between different Durable Objects?

Each Durable Object (cell) operates on its own independent SQLite connection stored in the thread-local DBS map. Because connections are scoped to individual cell IDs, transactions in one cell never interfere with another. The transaction_control function uses SQLite savepoints to handle nested transactions within a single cell, ensuring ACID semantics are maintained per-database (lines 154‑168).

What happens to the SQLite database when a Durable Object hibernates?

When a cell hibernates, Celld calls close(scope), which removes the connection from the thread-local DBS map and clears all per-cell prepared statement caches (lines 90‑99). This releases the file handle, allowing the underlying replicator to manage the database file and potentially migrate or back up the cell's data while the JavaScript runtime frees associated memory.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →