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

> Easily register and route between multiple backends in a single workspace. Learn to manage operations through a unified API with this complete guide.

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

---

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

```typescript
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`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/backends/worker-shell/worker-shell.ts)) implements this, while the **container** backend ([`packages/computer/src/backends/container/container-host.ts`](https://github.com/cloudflare/computer/blob/main/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`:

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

```typescript
// 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`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/workspace.ts) | Core registration logic, `#handleFor` routing, default selection |
| [`packages/computer/src/backends/worker-shell/worker-shell.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/backends/worker-shell/worker-shell.ts) | Callable backend implementation example |
| [`packages/computer/src/backends/container/container-host.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/backends/container/container-host.ts) | Non-callable backend with connection-based API |
| [`packages/computer/src/transport-failure.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/transport-failure.ts) | Error definitions for routing failures |
| [`packages/computer/tests/worker-backend.test.ts`](https://github.com/cloudflare/computer/blob/main/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`](https://github.com/cloudflare/computer/blob/main/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()`.