# Best Practices for Using the dofs Package in Cloudflare Computer

> Master the dofs package in Cloudflare Computer with essential best practices. Learn to initialize schema, manage cleanup with withDB, use SQLiteWorkspaceProvider, and ensure atomic persistence.

- Repository: [Cloudflare/computer](https://github.com/cloudflare/computer)
- Tags: best-practices
- Published: 2026-08-15

---

**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 like `mkdir`, `writeFile`, `readFile`, `symlink`, `watch`
- **SQLiteWorkspaceProvider** ([`src/provider.ts`](https://github.com/cloudflare/computer/blob/main/src/provider.ts)): Node-style `fs` API 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.

```typescript
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`](https://github.com/cloudflare/computer/blob/main/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.

```typescript
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`](https://github.com/cloudflare/computer/blob/main/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`](https://github.com/cloudflare/computer/blob/main/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.

```typescript
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`](https://github.com/cloudflare/computer/blob/main/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:

```typescript
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`](https://github.com/cloudflare/computer/blob/main/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:

```typescript
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:

```typescript
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`](https://github.com/cloudflare/computer/blob/main/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:

```typescript
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`](https://github.com/cloudflare/computer/blob/main/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`](https://github.com/cloudflare/computer/blob/main/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:

```bash
npm test --workspace @cloudflare/dofs

```

For custom logic, use the `withProvider` helper pattern from [`src/provider.test.ts`](https://github.com/cloudflare/computer/blob/main/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`](https://github.com/cloudflare/computer/blob/main/src/testing.ts) and [`src/testing-recording.ts`](https://github.com/cloudflare/computer/blob/main/src/testing-recording.ts) modules provide `SQLiteTestStorage` and `RecordingStorage` for building mocks and integration tests.

---

## Summary

- **Initialize once**: Call `initializeSchema` before any `dofs` API in your Durable Object constructor
- **Use `withDB` for tests**: Ensures automatic cleanup of temporary SQLite storage
- **Prefer `SQLiteWorkspaceProvider`**: Node-compatible API with full sync protocol support
- **Buffer large writes**: Use `openWriteBufferForCreateSync` + `releaseWriteBufferSync` for atomic batch operations
- **Check capabilities**: Inspect `readonly`, `supportsSymlinks`, `supportsWatch` before using features
- **Handle errors properly**: Catch and interpret codes from [`src/errors.ts`](https://github.com/cloudflare/computer/blob/main/src/errors.ts)
- **Leverage sync primitives**: Use `pushObjects`, `applyChanges`, and `coalesceChanges` for 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`](https://github.com/cloudflare/computer/blob/main/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.