Best Practices for Using the dofs Package in Cloudflare Computer
Initialize the schema once per Durable Object, wrap operations in withDB for automated cleanup, use SQLiteWorkspaceProvider for Node-compatible filesystem calls, and always release pending-write buffers to ensure atomic persistence.
The dofs package provides the core virtual filesystem that powers Cloudflare Computer, enabling durable, SQLite-backed storage with sync capabilities across Durable Objects. Whether you're building a workspace manager, implementing cross-DO file synchronization, or integrating with the computerd daemon via FUSE, following these best practices ensures reliability, performance, and compatibility with the sync protocol.
Architecture Overview: Three Independent Layers
The @cloudflare/dofs package is organized into three layers that can be used separately or composed together:
- Database layer (
Database,initializeSchema): Thin wrapper around Durable Object SQLite storage with schema initialization - Filesystem primitives (
src/fs/*): Low-level operations likemkdir,writeFile,readFile,symlink,watch - SQLiteWorkspaceProvider (
src/provider.ts): Node-stylefsAPI adapter compatible with@platformatic/vfs, used by the FUSE mount
Understanding this structure helps you choose the right abstraction for your use case.
Initialize the Schema Exactly Once Per Durable Object
Every dofs-backed Durable Object must call initializeSchema before any other API. The schema creates the vfs_* tables required by all filesystem operations.
import { Database, initializeSchema } from "@cloudflare/dofs";
export class WorkspaceDO extends DurableObject {
private readonly db: Database;
constructor(state: DurableObjectState, env: Env) {
super(state, env);
this.db = new Database(state.storage);
// Use a stable timestamp source for deterministic revision stamps
initializeSchema(this.db, Date.now);
}
}
Critical: Calling any filesystem method before initializeSchema throws "missing table" errors. The Date.now parameter provides a deterministic revision source for sync operations—avoid random or unstable timestamps.
As implemented in packages/dofs/README.md, this pattern is the foundation for all dofs usage in Cloudflare Computer.
Use SQLiteWorkspaceProvider for Familiar Node.js fs Operations
For most applications, SQLiteWorkspaceProvider offers the most practical interface. It exposes methods matching Node's fs module while maintaining full compatibility with the sync protocol.
import { withDB } from "@cloudflare/dofs/src/fs/with-db";
import { SQLiteWorkspaceProvider } from "@cloudflare/dofs/src/provider";
async function setupWorkspace() {
await withDB(async db => {
const provider = new SQLiteWorkspaceProvider(db, { now: () => Date.now() });
// Standard Node-style operations
provider.mkdirSync("/project", { mode: 0o755 });
provider.writeFileSync("/project/config.json", JSON.stringify({ version: 1 }));
console.log(provider.readFileSync("/project/config.json", "utf8"));
});
}
The withDB helper in src/fs/with-db.ts automatically tears down temporary SQLite storage, making it ideal for tests and short-lived operations. For production Durable Objects, instantiate SQLiteWorkspaceProvider directly with your persistent Database instance.
The comprehensive test suite in src/provider.test.ts validates all provider methods, including edge cases for renames, symlinks, and error conditions.
Leverage Pending-Write Buffers for Bulk Operations
Large file writes benefit from buffering to batch SQLite operations into single transactions. This pattern prevents intermediate partial states and improves performance.
await withDB(db => {
const p = new SQLiteWorkspaceProvider(db, { now: () => Date.now() });
// Open buffer for a new file
p.openWriteBufferForCreateSync("/large.bin", { mode: 0o644 });
// Write chunks at arbitrary offsets
p.writeRangeSync("/large.bin", Buffer.from("header data"), 0);
p.writeRangeSync("/large.bin", Buffer.from("body content"), 1024);
p.writeRangeSync("/large.bin", Buffer.from("footer"), 8192);
// Atomically commit all writes
p.releaseWriteBufferSync("/large.bin");
});
The buffer automatically flushes when operations require stable inodes—such as renameSync, linkSync, or unlinkSync. The test "pending-create flush on rename/link/unlink" in src/provider.test.ts (lines 70-95) verifies this behavior.
Always call releaseWriteBufferSync (or the async releaseWriteBuffer). Forgetting this leaves data in memory only, causing subsequent reads to see stale content.
Respect Capability Flags Before Using Advanced Features
SQLiteWorkspaceProvider advertises its capabilities through boolean flags. Check these before conditionally using features:
const provider = new SQLiteWorkspaceProvider(db, { now: () => Date.now() });
if (!provider.readonly) {
provider.writeFileSync("/writable.txt", "data");
}
if (provider.supportsSymlinks) {
provider.symlinkSync("/target", "/link");
}
if (provider.supportsWatch) {
// Set up filesystem watchers
}
These flags mirror the "capability flags" test assertions in src/provider.test.ts (lines 33-40). Respecting them ensures your code works across different mount configurations, including read-only or symlink-disabled environments.
Handle Rename, Link, and Unlink Semantics Correctly
The dofs package implements POSIX-compatible semantics with specific behaviors you must account for:
Hard links share inodes. linkSync creates a second directory entry with automatic reference counting:
p.writeFileSync("/original.txt", "shared content");
p.linkSync("/original.txt", "/hardlink.txt");
// Both paths reference the same inode; nlink === 2
Renames record atomic changes. Use coalesceChanges to fetch sync entries describing the mutation:
import { coalesceChanges } from "@cloudflare/dofs/src/sync/coalesce";
p.renameSync("/old/path", "/new/path");
const changes = [];
for await (const entry of coalesceChanges(db, cursor)) {
changes.push(entry); // Contains delete at old path, live entry at new path
}
Self-move protection throws EINVAL when renaming a directory into its own subtree. This check uses inode identity, not string prefix matching—preventing false positives with similar path names.
All error codes (ENOENT, EEXIST, ENOTEMPTY, ENOTDIR, EISDIR, EINVAL) are exported from src/errors.ts. Wrap operations in try/catch blocks and inspect these codes for robust error handling.
Use the Sync Protocol for Cross-DO Coordination
Files modified in one Durable Object can synchronize to others through the src/sync/* modules:
import { applyChanges } from "@cloudflare/dofs/src/sync/apply";
import { pushObjects } from "@cloudflare/dofs/src/sync/push";
// After local modifications, push to remote DOs
await pushObjects(db, ["blobHash1", "blobHash2"], { now: Date.now });
await applyChanges(db); // Apply any incoming changes
The sync helpers share the same Database instance, ensuring atomic revision stamps via incrementRev in src/rev.ts. For production use, the @cloudflare/computer-rpc package provides high-level RPC wiring—use raw sync primitives only when you need custom coordination logic.
Avoid Common Pitfalls
| Pitfall | Symptom | Solution |
|---|---|---|
| Writing to read-only mount | EACCES or silent failures |
Verify mode !== "read-only"; use invalidateReadOnlyMountCache after mode changes |
| Unreleased write buffer | Data never persists, reads see stale content | Always call releaseWriteBufferSync after writes |
| Non-atomic overwrites | Partial data loss on crash | Use writeFileSync for small files; buffer + renameSync for large atomic updates |
Missing withFileTypes |
readdirSync returns strings instead of Dirent objects |
Pass { withFileTypes: true } when you need rich file metadata |
The test "renameSync overwrite evicts the displaced destination's buffer" in src/provider.test.ts (lines 56-78) demonstrates the atomic rename pattern for safe file updates.
Testing and Debugging
Run the package test suite to verify behavior contracts:
npm test --workspace @cloudflare/dofs
For custom logic, use the withProvider helper pattern from src/provider.test.ts. It creates isolated SQLiteWorkspaceProvider instances backed by in-memory SQLite, matching production semantics with fast test execution.
The src/testing.ts and src/testing-recording.ts modules provide SQLiteTestStorage and RecordingStorage for building mocks and integration tests.
Summary
- Initialize once: Call
initializeSchemabefore anydofsAPI in your Durable Object constructor - Use
withDBfor tests: Ensures automatic cleanup of temporary SQLite storage - Prefer
SQLiteWorkspaceProvider: Node-compatible API with full sync protocol support - Buffer large writes: Use
openWriteBufferForCreateSync+releaseWriteBufferSyncfor atomic batch operations - Check capabilities: Inspect
readonly,supportsSymlinks,supportsWatchbefore using features - Handle errors properly: Catch and interpret codes from
src/errors.ts - Leverage sync primitives: Use
pushObjects,applyChanges, andcoalesceChangesfor cross-DO coordination
Frequently Asked Questions
How do I set up dofs in a new Durable Object?
Install @cloudflare/dofs, import Database and initializeSchema, create a Database instance from state.storage, and call initializeSchema with a stable timestamp source. This prepares the SQLite schema for all subsequent filesystem operations.
What's the difference between filesystem primitives and SQLiteWorkspaceProvider?
The primitives in src/fs/* provide low-level operations for advanced use cases. SQLiteWorkspaceProvider in src/provider.ts composes these into a Node.js fs-compatible API that's easier to use and integrates with @platformatic/vfs and FUSE mounts.
How do I synchronize files between Durable Objects?
Use the sync protocol from src/sync/*: pushObjects to send changes, applyChanges to receive them, and coalesceChanges to query mutation history. These share the same Database instance for atomic revision tracking. For production RPC, use @cloudflare/computer-rpc rather than raw primitives.
Why are my large file writes not persisting?
You likely opened a write buffer with openWriteBufferForCreateSync but forgot to call releaseWriteBufferSync. Data in the pending-write buffer stays in memory until released or automatically flushed by certain operations. Always explicitly release buffers when writes complete.
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 →