# How to Implement a Custom Mount Provider for Cloudflare R2 or Other Storage Backends in Computer

> Learn to implement a custom mount provider for Cloudflare R2 or other storage backends in Computer. Follow our guide to create an EagerMount object with a materialize method.

- Repository: [Cloudflare/computer](https://github.com/cloudflare/computer)
- Tags: how-to-guide
- Published: 2026-08-07

---

**Implement a custom mount provider by creating a function that returns an `EagerMount` object with a `materialize(api)` method, following the contract defined in [`packages/computer/src/mounts/types.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/mounts/types.ts).**

The Computer library lets you mount external storage—R2, GitHub, or any custom backend—into a virtual workspace via a lightweight, type-driven provider pattern. A mount provider is simply a factory function that conforms to the `EagerMount` interface and streams files into the VFS using the `MountWriteAPI` during workspace initialization.

## Understanding the Mount Provider Architecture

At its core, every mount provider must satisfy two contracts defined in the source:

- **`MountBase`** ([`packages/computer/src/mounts/types.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/mounts/types.ts)): Specifies `kind` (identifier string), `mode` (`"read-only"` or `"read-write"`), and optional `maxBytes`/`maxEntries` caps.
- **`EagerMount`**: Extends `MountBase` with `strategy: "eager"` and a `materialize(api: MountWriteAPI)` async method.

The workspace invokes `materialize()` exactly once—the first cold start—then persists an indexed flag in `_vfs_mounts` to skip re-materialization on subsequent boots ([`packages/computer/src/mounts/registry.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/mounts/registry.ts)).

## Step 1: Define the Provider Interface

Start by modeling your backend's minimal surface area. The R2 implementation demonstrates that you only need three operations: list objects, get object contents, and optionally handle pagination cursors.

```typescript
// packages/computer/src/mounts/providers/custom.ts
import type { EagerMount, MountWriteAPI } from "../types.js";

export interface MyBackend {
  list(options?: { prefix?: string; cursor?: string; limit?: number }): Promise<{
    objects: Array<{ key: string; size: number }>;
    truncated: boolean;
    cursor?: string;
  }>;
  get(key: string): Promise<{ body: ReadableStream<Uint8Array>; size: number } | null>;
}

export interface MyBackendOptions {
  prefix?: string;
  listLimit?: number;
  concurrency?: number;
  maxBytes?: number;
  maxEntries?: number;
}

```

## Step 2: Implement Pagination and Concurrency Helpers

The R2 provider in [`packages/computer/src/mounts/providers/r2.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/mounts/providers/r2.ts) uses two reusable patterns: async generator pagination and bounded-concurrency fan-out.

**Pagination helper** streams all keys without buffering the entire dataset into memory:

```typescript
function normalisePrefix(p?: string): string {
  if (!p) return "";
  return p.endsWith("/") ? p : `${p}/`;
}

async function* paginate(
  backend: MyBackend,
  prefix: string,
  limit: number,
): AsyncGenerator<{ key: string; size: number }> {
  let cursor: string | undefined;
  while (true) {
    const page = await backend.list({ prefix: prefix || undefined, cursor, limit });
    for (const obj of page.objects) yield obj;
    if (!page.truncated || !page.cursor) return;
    cursor = page.cursor;
  }
}

```

**Bounded-concurrency runner** limits simultaneous `get` calls to respect backend rate limits:

```typescript
async function runBounded<T>(tasks: Array<() => Promise<T>>, concurrency: number): Promise<void> {
  if (concurrency < 1) concurrency = 1;
  let next = 0;
  const workers: Promise<void>[] = [];
  let firstError: unknown;

  for (let i = 0; i < Math.min(concurrency, tasks.length); i++) {
    workers.push(
      (async () => {
        while (true) {
          if (firstError !== undefined) return;
          const idx = next++;
          if (idx >= tasks.length) return;
          try {
            await tasks[idx]();
          } catch (e) {
            if (firstError === undefined) firstError = e;
            return;
          }
        }
      })(),
    );
  }

  await Promise.all(workers);
  if (firstError !== undefined) throw firstError;
}

```

## Step 3: Build the EagerMount Factory

Combine these helpers into the final provider function. The `materialize` method receives `api: MountWriteAPI` with two critical methods: `writeFile(path, stream)` and `mkdir(path)`.

```typescript
export function MyBackendMount(backend: MyBackend, opts: MyBackendOptions = {}): EagerMount {
  const prefix = normalisePrefix(opts.prefix);
  const listLimit = opts.listLimit ?? 1000;
  const concurrency = opts.concurrency ?? 8;

  return {
    kind: "my-backend",
    mode: "read-only",
    strategy: "eager",
    maxBytes: opts.maxBytes,
    maxEntries: opts.maxEntries,

    async materialize(api: MountWriteAPI) {
      // Collect all keys via pagination.
      const keys: Array<{ key: string; size: number }> = [];
      for await (const obj of paginate(backend, prefix, listLimit)) {
        keys.push(obj);
      }

      // Fan-out get operations with bounded concurrency.
      const tasks = keys.map((entry) => async () => {
        const payload = await backend.get(entry.key);
        if (!payload) {
          throw new Error(`MyBackendMount: object vanished mid-materialize: ${entry.key}`);
        }
        const relKey = prefix ? entry.key.slice(prefix.length) : entry.key;
        if (!relKey) return; // Skip the prefix-object itself.

        await api.writeFile(`${api.root}/${relKey}`, payload.body);
      });

      await runBounded(tasks, concurrency);
    },
  };
}

```

Key implementation details from the R2 reference:

- **`api.root`** contains the absolute VFS path (e.g., `/workspace/my`) prepopulated by the workspace.
- **Streaming**: Pass the raw `ReadableStream<Uint8Array>` directly to `writeFile`—no buffering required.
- **Prefix stripping**: Remove the configured prefix so [`data/file.txt`](https://github.com/cloudflare/computer/blob/main/data/file.txt) mounts as [`/workspace/my/file.txt`](https://github.com/cloudflare/computer/blob/main//workspace/my/file.txt), not [`/workspace/my/data/file.txt`](https://github.com/cloudflare/computer/blob/main//workspace/my/data/file.txt).

## Step 4: Export and Register the Provider

Add your provider to the public exports in [`packages/computer/src/index.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/index.ts):

```typescript
export { MyBackendMount } from "./mounts/providers/custom.js";

```

Instantiate the workspace with a `mounts` map. Each key is an absolute VFS path; each value is the mount instance:

```typescript
import { Workspace } from "@cloudflare/computer";
import { MyBackendMount } from "@cloudflare/computer/mounts/providers/custom";

const backend = env.MY_BACKEND as MyBackend;

const ws = await Workspace.create({
  sessionId: "user-123",
  db: env.DATABASE,
  mounts: {
    "/workspace/my": MyBackendMount(backend, {
      prefix: "data/",
      maxBytes: 50_000_000,
      concurrency: 16,
    })
  },
});

```

The `Workspace` constructor (in [`packages/computer/src/workspace.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/workspace.ts)) iterates the `mounts` object, invokes `materialize()` for any unindexed mount, and records completion in `_vfs_mounts`.

## R2-Specific Pattern for Cloudflare Workers

For **Cloudflare R2 specifically**, use the built-in `R2Bucket` provider in [`packages/computer/src/mounts/providers/r2.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/mounts/providers/r2.ts). The pattern is identical to custom providers—the R2 binding itself satisfies the `MyBackend` interface above:

```typescript
import { R2Bucket } from "@cloudflare/computer/mounts/providers/r2";

const ws = await Workspace.create({
  mounts: {
    "/workspace/r2": R2Bucket(env.MY_BUCKET, {
      prefix: "uploads/",
      maxEntries: 10_000,
    })
  },
});

```

The R2 provider adds R2-specific optimizations: automatic `HEAD` request avoidance when size is known, and integration with R2's native `ReadableStream` implementation.

## Key Source Files Reference

| File | Purpose |
|------|---------|
| [`packages/computer/src/mounts/types.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/mounts/types.ts) | `MountBase`, `EagerMount`, `MountWriteAPI` definitions |
| [`packages/computer/src/mounts/providers/r2.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/mounts/providers/r2.ts) | Reference implementation showing pagination, streaming, concurrency |
| [`packages/computer/src/workspace.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/workspace.ts) | Workspace orchestration and mount lifecycle |
| [`packages/computer/src/mounts/registry.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/mounts/registry.ts) | Indexed flag persistence in `_vfs_mounts` |
| [`examples/worker-shell/src/index.ts`](https://github.com/cloudflare/computer/blob/main/examples/worker-shell/src/index.ts) | Real-world R2 mount example |

## Summary

- **Custom mount providers** implement the `EagerMount` contract from [`packages/computer/src/mounts/types.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/mounts/types.ts).
- **The `materialize(api)` method** receives `MountWriteAPI` to stream files via `writeFile` and create directories via `mkdir`.
- **Pagination and bounded concurrency** are reusable patterns from the R2 reference implementation.
- **Registration** happens through the `mounts` option in `Workspace.create()`, with automatic indexed-flag caching to skip re-materialization.
- **Export your provider** in [`packages/computer/src/index.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/index.ts) for consumer access.

## Frequently Asked Questions

### What is the difference between `EagerMount` and lazy mount strategies?

**`EagerMount` with `strategy: "eager"` materializes all files during workspace cold start**, blocking until the backend is fully indexed. Computer currently optimizes for eager mounts because they provide predictable, filesystem-like latency after initialization. Lazy or on-demand strategies are not yet implemented in the public API according to [`packages/computer/src/mounts/types.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/mounts/types.ts).

### How do I enforce storage limits like `maxBytes` or `maxEntries`?

**The workspace enforces caps automatically** when the mount specifies `maxBytes` or `maxEntries` in its returned object. During `materialize()`, the workspace tracks cumulative bytes and entry count; exceeding either limit aborts materialization and throws. Your provider should not pre-filter—return the full dataset and let the workspace apply limits consistently.

### Can I implement write support for my custom backend?

**Change `mode` to `"read-write"` and implement a `MountReadWriteAPI`**, though the current source in [`packages/computer/src/mounts/types.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/mounts/types.ts) shows `mode` as primarily informational. Full write support requires additional methods beyond `materialize`, such as `sync()` or change watchers. The R2 provider currently uses `"read-only"`; inspect future releases for expanded `MountWriteAPI` surface area.

### Why does materialization only run once per mount path?

**The workspace persists an `indexed` flag** in the `_vfs_mounts` SQLite table via [`packages/computer/src/mounts/registry.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/mounts/registry.ts). On subsequent boots, the workspace checks this flag and skips `materialize()` if set. To force re-materialization (e.g., after backend changes), clear the mount's entry from `_vfs_mounts` or use a different mount path.