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
#registeredBackendIdsSet - Builds lookup maps in
#backendsByIdfor 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 backendpull(backendId?)— retrieve updates from a specific backendexec(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
backendsarray innew Workspace() - Route explicitly with
backendIdarguments 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →