How to Register and Route Between Multiple Backends on a Single Workspace: A Complete Guide

Register backends via the backends option in the Workspace constructor, then route calls by passing a backendId argument or rely on the default (first) backend to handle all operations through a unified API.

The Cloudflare Computer library lets you orchestrate diverse compute environments—from Worker shells to containers to remote git repositories—within a single Workspace instance. By registering multiple backends, you gain a consistent interface for exec(), connect(), push(), and pull() regardless of where the actual computation happens.

Registering Multiple Backends at Construction

Backend registration happens during Workspace instantiation in packages/computer/src/workspace.ts. The constructor accepts an options.backends array where each entry requires a unique id and implements the backend interface.

Inside the constructor (lines 308–363), the code:

  • Deduplicates IDs using a private #registeredBackendIds Set
  • Builds lookup maps in #backendsById for O(1) resolution
  • Designates the first backend as #defaultBackendId (line 365)
import { Workspace } from "@cloudflare/computer";
import { WorkerShellBackend } from "@cloudflare/computer/backends/worker-shell";
import { ContainerBackend } from "@cloudflare/computer/backends/container";

const ws = new Workspace({
  db, // SQLiteWorkspaceProvider already created
  backends: [
    {
      id: "worker",           // unique identifier across workspace
      type: "worker-shell",   // backend implementation name
      callable: true,         // supports exec() calls
      ctor: WorkerShellBackend,
    },
    {
      id: "container",
      type: "container",
      callable: false,        // no exec()—use run() or connect() instead
      ctor: ContainerBackend,
    },
  ],
});

The callable flag determines whether a backend supports direct command execution. The worker-shell backend (packages/computer/src/backends/worker-shell/worker-shell.ts) implements this, while the container backend (packages/computer/src/backends/container/container-host.ts) requires connection-based interaction.

Routing Calls to Specific Backends

Every public method that communicates with backends accepts an optional backendId parameter. When omitted, the workspace routes to #defaultBackendId.

The private #handleFor(id) method (lines 1011–1034) resolves the correct BackendHandle:

// Implicit routing: uses default backend ("worker")
await ws.exec("ls", { cwd: "/" });

// Explicit routing: target container by ID
await ws.connect("container");
await ws.run("container", async (backend) => {
  // `backend` is a BackendHandle for the container
  await backend.rpc.fuse.mount("/app");
});

Other routable operations include:

  • push(backendId?) — sync state to a specific backend
  • pull(backendId?) — retrieve updates from a specific backend
  • exec(command, opts, backendId?) — execute commands on callable backends

Per-Backend Sync with pull() and push()

When each backend maintains its own sync cursor, you can isolate operations to prevent cross-contamination:

// Pull only from worker-shell backend
await ws.pull("worker");

// Pull only from container backend
await ws.pull("container");

This pattern proves essential when backends have divergent filesystem states or different network latencies.

Lazy Connection and Connection Deduplication

The first use of any backend ID triggers connection establishment. The workspace stores the connection promise in a private #connecting Map (lines 267–274), ensuring that concurrent callers for the same backend share a single connection attempt rather than spawning duplicates.


Caller A → ws.connect("container") → creates connection promise
Caller B → ws.connect("container") → awaits same promise

This lazy initialization keeps workspace startup fast and prevents resource waste for backends that may never be used in a session.

Error Handling for Invalid Backend IDs

Requesting a nonexistent backend ID triggers a descriptive error from the validation logic (lines 856–860). The error message lists all registered backend IDs, making debugging straightforward:


Error: Unknown backend "vm". Registered backends: ["worker", "container"]

This validation occurs in #handleFor() before any network operations begin, failing fast rather than timing out on a phantom backend.

Backend Registration and Routing: Key Source Files

File Purpose
packages/computer/src/workspace.ts Core registration logic, #handleFor routing, default selection
packages/computer/src/backends/worker-shell/worker-shell.ts Callable backend implementation example
packages/computer/src/backends/container/container-host.ts Non-callable backend with connection-based API
packages/computer/src/transport-failure.ts Error definitions for routing failures
packages/computer/tests/worker-backend.test.ts Integration tests for multi-backend scenarios

Summary

  • Register multiple backends via the backends array in new Workspace()
  • Route explicitly with backendId arguments or implicitly through the default
  • Connect lazily with automatic promise deduplication per backend
  • Validate early—unknown IDs throw immediately with registered alternatives
  • Sync independently per backend when cursors or states diverge

The Workspace abstraction in cloudflare/computer transforms heterogeneous compute environments into a single, addressable surface.

Frequently Asked Questions

How does the Workspace handle duplicate backend IDs?

The constructor throws during instantiation if duplicate id values appear in the backends array. The #registeredBackendIds Set enforces uniqueness before any backend initialization occurs.

Can I change the default backend after creation?

No. The #defaultBackendId is set once at construction (line 365) from the first backend in the array. To use a different default, reorder the backends array or always pass explicit backendId arguments.

What happens if a backend connection fails?

The connection promise stored in #connecting rejects, and subsequent calls to the same ID will retry connection. Errors propagate from packages/computer/src/transport-failure.ts with backend-specific context.

Do all backends support the exec() method?

Only backends with callable: true support exec(). The container backend, for example, requires connect() or run() instead. Check the callable property in your backend configuration before calling ws.exec().

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 →