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

> Discover how Celld uses per-cell SQLite databases for durable object storage. Learn about WAL mode, custom authorizers, and savepoint transactions for robust API implementation.

- Repository: [Deno/celld](https://github.com/denoland/celld)
- Tags: internals
- Published: 2026-08-10

---

**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`](https://github.com/denoland/celld/blob/main/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](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs)). 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](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs))
- **`alarms`** – Persisted alarm data ([lines 29‑33](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs))
- **`cell_metadata`** – Optional cell-level metadata ([lines 36‑38](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs))

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](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs)).

### 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](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs))
- **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](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs) and [lines 60‑63](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs))

### 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](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs))

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`](https://github.com/denoland/celld/blob/main/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](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs))
- **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](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs))

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](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs)).

## The JavaScript Harness ([`crates/celld/js/harness.js`](https://github.com/denoland/celld/blob/main/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](https://github.com/denoland/celld/blob/main/crates/celld/js/harness.js)). 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](https://github.com/denoland/celld/blob/main/crates/celld/js/harness.js), [lines 81‑92](https://github.com/denoland/celld/blob/main/crates/celld/js/harness.js), [lines 94‑106](https://github.com/denoland/celld/blob/main/crates/celld/js/harness.js)). Alarm methods (`setAlarm`, `getAlarm`, `deleteAlarm`) map directly to `__alarm_*` helpers ([lines 42‑57](https://github.com/denoland/celld/blob/main/crates/celld/js/harness.js)).

### 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](https://github.com/denoland/celld/blob/main/crates/celld/js/harness.js))

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](https://github.com/denoland/celld/blob/main/crates/celld/js/harness.js)).

## Practical Code Examples

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

### Basic KV Operations

```javascript
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

```javascript
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

```javascript
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`](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs) ([lines 14‑22](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs)), 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](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs)). Additionally, `sqlite3_limit` caps SQL length and column counts to prevent resource-exhaustion attacks ([lines 64‑80](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs)).

### 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](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs)).

### 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](https://github.com/denoland/celld/blob/main/crates/celld/storage.rs)). 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.