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:
- Canonicalize the path via
canonicalizePath(src/path.ts) - Enter a transaction using
db.transactionSync() - Modify
vfs_*tables (insert rows for directories, write blobs for files) - Update the revision via
incrementRev() - Invalidate caches via
invalidateResolveExact(src/fs/resolveCache.ts) - 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 supplymtimeandctimetimestamps - The
Databaseinstance 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/dofsprovides 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 insrc/fs/*, designed to be composed into higher-level APIs like theSQLiteWorkspaceProvider. - Revision tracking via
incrementRev()and cache invalidation viainvalidateResolveExactkeep 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →