How to Implement Filesystem Primitives Against SQLite Storage in Durable Objects

The @cloudflare/dofs package provides a production-ready, SQLite-backed virtual filesystem that runs entirely inside Cloudflare Durable Objects, exposing synchronous Node.js-style primitives like mkdir, writeFile, and readFile through a transactional storage layer.

Durable Objects (DOs) provide stateful coordination for serverless applications, but they lack a native filesystem interface. By leveraging the @cloudflare/dofs library—extracted from the Cloudflare Workers runtime—you can mount a POSIX-like filesystem inside any DO using SQLite as the storage engine. This implementation stores file metadata, directory entries, and blob contents in dedicated vfs_* tables while exposing low-level primitives that handle path canonicalization, revision tracking, and cache invalidation automatically.

Architecture of the SQLite Filesystem Layer

The implementation follows a strict three-tier architecture that separates storage concerns from filesystem semantics.

Database Wrapper

At the lowest level, the Database class in src/storage.ts wraps the DO’s DurableObjectStorage interface. It provides synchronous transaction support via transactionSync(), which uses SQLite SAVEPOINTs for nested calls, and convenience methods like run(), one(), all(), and scalar() for SQL execution. The wrapper also maintains an inTransaction flag consumed by the path resolution cache.

SQLite Schema

The schema lives in src/schema/core.ts and src/schema/sync.ts, defining tables such as vfs_nodes (inode metadata), vfs_dirents (directory entries), and vfs_blob_bytes (content-addressed file data). The initializeSchema() function creates these tables on first boot, while incrementRev() generates monotonic revision numbers required by the sync protocol. Every mutating primitive writes this revision to vfs_nodes.rev, enabling tombstone tracking for distributed consistency.

Filesystem Primitives

Each primitive resides under src/fs/ and follows an identical workflow:

  1. Canonicalize the path via canonicalizePath (src/path.ts)
  2. Enter a transaction using db.transactionSync()
  3. Modify vfs_* tables (insert rows for directories, write blobs for files)
  4. Update the revision via incrementRev()
  5. Invalidate caches via invalidateResolveExact (src/fs/resolveCache.ts)
  6. Guard read-only mounts using assertNotReadOnly (src/fs/mount-guard.ts)

Setting Up Filesystem Primitives in a Durable Object

To use the filesystem, instantiate the Database wrapper inside your DO constructor and initialize the schema before performing any operations.

// src/workspace-do.ts
import { Database, initializeSchema } from "@cloudflare/dofs";
import { mkdir, writeFile, readFile, readdir } from "@cloudflare/dofs/src/fs";

export class WorkspaceDO extends DurableObject {
  private readonly db: Database;

  constructor(state: DurableObjectState, env: Env) {
    super(state, env);
    this.db = new Database(state.storage);
    // Initialize vfs_* tables on first run
    initializeSchema(this.db, Date.now);
  }

  async addSampleFile() {
    const now = Date.now;
    mkdir(this.db, "/example", { recursive: true }, now);
    writeFile(
      this.db, 
      "/example/hello.txt", 
      Buffer.from("👋 Hello!"), 
      {}, 
      now
    );
  }

  async getSampleFile(): Promise<string> {
    const buf = readFile(this.db, "/example/hello.txt");
    return Buffer.from(buf).toString("utf-8");
  }

  async listRoot(): Promise<string[]> {
    const entries = readdir(this.db, "/");
    return entries.map(e => e.name);
  }
}

Critical implementation details:

  • All primitives are synchronous but run inside an atomic transaction that auto-rolls back on error
  • Pass Date.now (or a custom clock) to supply mtime and ctime timestamps
  • The Database instance must persist for the lifetime of the DO to maintain cache consistency

Common Filesystem Operations

Creating Nested Directories

The mkdir primitive in src/fs/mkdir.ts supports recursive creation and POSIX mode bits.

import { mkdir } from "@cloudflare/dofs/src/fs/mkdir";

// Creates /a/b/c, creating any missing intermediate directories
mkdir(db, "/a/b/c", { recursive: true, mode: 0o755 }, Date.now);

Under the hood, this inserts a row into vfs_nodes with type: 'directory' and adds corresponding entries to vfs_dirents for each parent-child relationship.

Writing Files with Blob Storage

writeFile (located in src/fs/writeFile.ts) handles content addressing automatically. It inserts byte arrays into vfs_blob_bytes and updates the inode’s rev column.

import { writeFile } from "@cloudflare/dofs/src/fs/writeFile";

const data = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
writeFile(db, "/a/b/c/greeting.txt", data, {}, Date.now);

Reading Files and Listing Directories

readFile (src/fs/readFile.ts) returns a Uint8Array via the content-addressed blob cache, while readdir (src/fs/readdir.ts) paginates through vfs_dirents.

import { readFile } from "@cloudflare/dofs/src/fs/readFile";
import { readdir } from "@cloudflare/dofs/src/fs/readdir";

const content = readFile(db, "/a/b/c/greeting.txt");
console.log(new TextDecoder().decode(content)); // → Hello

const entries = readdir(db, "/a/b/c");
console.log(entries.map(e => e.name));

The entries array contains objects with name, type, size, and mtime properties sourced from the joined vfs_nodes and vfs_dirents tables.

Advanced Features and Constraints

Transactional Consistency

Every primitive operates within db.transactionSync(), ensuring that path resolution, inode creation, and revision bumping are atomic. If a primitive throws, SQLite automatically rolls back to the SAVEPOINT established at the transaction boundary.

Cache Invalidation

When directory entries change, invalidateResolveExact (src/fs/resolveCache.ts) drops stale path-to-inode mappings. This prevents stale reads when a file is moved or deleted between operations.

Read-Only Mount Guards

For FUSE integrations where the filesystem is mounted read-only, assertNotReadOnly and assertNotInReadOnlyMount (src/fs/mount-guard.ts) throw before any mutating SQL is executed, protecting against accidental writes in the computerd daemon context.

Summary

  • @cloudflare/dofs provides a synchronous, SQLite-backed filesystem API for Durable Objects through three layers: storage wrapper, schema management, and primitive implementations.
  • Initialize once using initializeSchema() in the DO constructor before calling any filesystem methods.
  • All operations are transactional via Database.transactionSync(), ensuring atomicity across metadata and blob updates.
  • Primitives are low-level (mkdir, writeFile, readFile, readdir, etc.) and live in src/fs/*, designed to be composed into higher-level APIs like the SQLiteWorkspaceProvider.
  • Revision tracking via incrementRev() and cache invalidation via invalidateResolveExact keep distributed views consistent.

Frequently Asked Questions

How does the filesystem handle concurrent writes from multiple DO instances?

Each Durable Object is single-threaded, but the SQLite transaction model in src/storage.ts ensures atomicity for concurrent requests hitting the same DO. The transactionSync() method uses SQLite SAVEPOINTs to handle nested calls, and the incrementRev() function provides monotonic versioning that the sync protocol uses to resolve conflicts across distributed instances.

Can I use standard Node.js fs promises with this implementation?

Not directly. The primitives in @cloudflare/dofs are synchronous and operate on the Database class rather than file descriptors. However, the SQLiteWorkspaceProvider in the @platformatic/vfs adapter wraps these primitives into a Node-compatible fs interface that the computerd daemon mounts via FUSE, enabling standard fs.promises usage at a higher abstraction layer.

What happens if a write operation fails midway?

Because every primitive runs inside db.transactionSync(), any thrown error triggers an automatic ROLLBACK to the SAVEPOINT established at the start of the operation. This ensures that partial writes—such as an inode insertion without the corresponding blob write—never leave the database in an inconsistent state. The inTransaction flag in src/storage.ts tracks nesting depth to handle complex operations like recursive mkdir.

Where is file content actually stored in the SQLite database?

File contents are stored in the vfs_blob_bytes table defined in src/schema/core.ts, using content-addressed storage keyed by hash. The vfs_nodes table stores metadata (size, permissions, revision) and references the blob via its hash, while vfs_dirents maintains the directory tree structure. This separation allows deduplication and efficient garbage collection via the gc primitive.

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 →