# How the withWorkspace Decorator Manages Durable Object Lifecycle and Stub Disposal in Cloudflare Computer

> Learn how the withWorkspace decorator in cloudflare/computer synchronizes Durable Object lifecycles and disposes of stubs, ensuring efficient resource management.

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

---

**The `withWorkspace` decorator synchronizes Durable Object lifecycles with Workspace instances by storing the workspace under a private symbol and exposing a thin RPC stub via `__getWorkspaceStub()`.**

The `withWorkspace` decorator in the Cloudflare Computer repository provides a mixin pattern that equips Durable Objects (DOs) with fully-featured `Workspace` instances. This architecture ensures that the DO's lifetime governs the Workspace and its RPC stub, preventing resource leaks across the Workers RPC boundary. Understanding how this decorator manages construction, storage, and disposal is essential for building reliable applications with Cloudflare's computer platform.

## Workspace Construction and Private Storage

When you apply the `withWorkspace` decorator to a Durable Object class, the mixin intercepts the constructor to instantiate a local Workspace immediately after the base class initializes.

### The Constructor Flow

In [`packages/computer/src/with-workspace.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/with-workspace.ts), the decorator accepts a base class and an `options` callback. When your Durable Object is instantiated, the mixin calls `super(...args)` first, then invokes `options(self)` where `self` is the fresh DO instance. This callback receives the DO's `ctx` and `env`, allowing you to extract the Durable Object storage, session ID, and backend configurations. The returned `WorkspaceOptions` are passed to `new Workspace(...)`, which initializes a local SQLite-backed store and sets up the runtime environment.

### Symbol-Based Encapsulation

The Workspace instance is stored on the DO using a **module-private symbol** (`WORKSPACE`) defined in [`with-workspace.ts`](https://github.com/cloudflare/computer/blob/main/with-workspace.ts). This symbol-keyed field is invisible to standard property enumeration and, critically, is **not serialized** across the RPC boundary. Only the in-process helper `getWorkspace()` (exported from [`packages/computer/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/client.ts)) can access this symbol to retrieve the Workspace instance, ensuring that external RPC callers cannot directly access internal state.

## RPC Stub Exposure via __getWorkspaceStub

The decorator bridges the gap between the local Workspace and remote Workers by exposing a single RPC-friendly method that returns a serializable stub.

### The RPC-Friendly Gateway

The mixin adds `__getWorkspaceStub()` to the prototype (lines 71-77 of [`with-workspace.ts`](https://github.com/cloudflare/computer/blob/main/with-workspace.ts)). This is the only shape that Workers RPC can dispatch to, as RPC requires plain-named prototype methods. When a Worker calls `env.MyDO.get(id)` and then invokes `getWorkspace(stub)`, the RPC layer dispatches to this method on the remote DO.

The implementation awaits `ws.ready()` to ensure mount indexing completes, then returns `ws.stub()`. According to [`packages/computer/src/workspace.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/workspace.ts) (lines 15-21), the `stub()` method creates a `WorkspaceStub`—a lazy RPC target that holds no resources itself and simply forwards operations back to the original Workspace instance.

### Lazy Stub Instantiation

Because the stub is created on-demand after the workspace is ready, the Durable Object's lifetime naturally governs the stub's existence. The stub acts as a thin proxy that lives only while the DO remains active, ensuring no dangling references persist beyond the DO's lifecycle.

## Lifecycle Alignment and Resource Management

The `withWorkspace` architecture ensures that Workspace resources are tightly bound to the Durable Object's lifecycle, with automatic cleanup when the DO shuts down.

### Implicit Stub Disposal

The stub itself does not implement an explicit `dispose` method; disposal is implicit through garbage collection. Since `WorkspaceStub` only holds a promise to the real Workspace and manages no native resources, freeing the Durable Object (or calling `Workspace.close()`) automatically invalidates in-flight RPC calls. In [`packages/computer/src/workspace.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/workspace.ts) (lines 48-72), the `close()` method tears down cached handles, shells, and connection generation counters, rejecting pending RPCs and rendering stubs unreachable.

### Error Handling and Connection Cleanup

If a transport error occurs during command execution, `watchStreamForTransportError` (lines 106-114 of [`workspace.ts`](https://github.com/cloudflare/computer/blob/main/workspace.ts)) triggers the `onError` callback. This callback invokes the private `#invalidateHandle` method (lines 73-78), which clears cached backend handles. This mechanism ensures that failing stubs do not leave dangling connections or orphaned resources, maintaining the integrity of the Durable Object's resource pool.

## Implementation Examples

Define a Durable Object class that automatically receives a Workspace:

```typescript
import { DurableObject } from "cloudflare:workers";
import { withWorkspace } from "@cloudflare/computer";

export class MyDO extends withWorkspace(
  class extends DurableObject<Env> {},
  (self) => ({
    storage: self.ctx.storage,
    sessionId: self.ctx.id.toString(),
    backends: [{ id: "default", type: "container" }],
  })
) {}

```

Access the Workspace from inside the Durable Object using the symbol-based getter:

```typescript
import { getWorkspace } from "@cloudflare/computer/client";

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const ws = getWorkspace(this); // Returns the local Workspace instance
    await ws.fs.writeFile("/hello.txt", "world");
    return new Response(await ws.fs.readFile("/hello.txt", "utf8"));
  }
};

```

Obtain a stub from a Worker and execute operations over RPC:

```typescript
import { getWorkspace } from "@cloudflare/computer/client";

export async function onRequest(request: Request, env: Env) {
  const id = env.MyDO.idFromName("session");
  const stub = env.MyDO.get(id);
  
  // Convert DO stub to Workspace stub via RPC
  const wsStub = await getWorkspace(stub);
  
  // Filesystem operations traverse the RPC boundary
  await wsStub.fs.writeFile("/msg.txt", "hello from worker");
  return new Response(await wsStub.fs.readFile("/msg.txt", "utf8"));
}

```

## Summary

- The `withWorkspace` decorator in [`packages/computer/src/with-workspace.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/with-workspace.ts) wraps Durable Object construction to instantiate a local Workspace immediately after `super()` completes.
- The Workspace is stored under a private symbol (`WORKSPACE`) that is invisible to RPC serialization, accessible only via the `getWorkspace()` helper.
- The `__getWorkspaceStub()` method exposes a RPC-compatible endpoint that returns a `WorkspaceStub` created by `Workspace.stub()` in [`packages/computer/src/workspace.ts`](https://github.com/cloudflare/computer/blob/main/packages/computer/src/workspace.ts).
- Stub disposal is implicit: when the Durable Object closes or is reclaimed, `Workspace.close()` tears down resources and invalidates stubs, while transport errors trigger `#invalidateHandle` to clear cached connections.

## Frequently Asked Questions

### How does withWorkspace prevent the Workspace from being serialized across RPC?

The decorator stores the Workspace instance using a **module-private symbol** as the key. Since symbol-keyed properties are not enumerable and are excluded from Workers RPC serialization, the full Workspace object remains server-side. Only the thin `WorkspaceStub` returned by `__getWorkspaceStub()` crosses the RPC boundary.

### What happens if I call getWorkspace on a Durable Object stub instead of an instance?

When you pass a Durable Object stub (obtained via `env.MyDO.get(id)`) to `getWorkspace()`, the function detects the remote reference and invokes `__getWorkspaceStub()` over RPC. This returns a `WorkspaceStub` that proxies all method calls back to the original DO's Workspace instance, maintaining the same API surface whether local or remote.

### Why doesn't the WorkspaceStub have an explicit dispose method?

The stub is designed as a **lazy RPC target** that holds no native resources or handles. It merely forwards operations to the underlying Workspace. Resource cleanup occurs when the Durable Object calls `Workspace.close()` or is garbage collected, which automatically rejects pending RPC calls and releases the SQLite database and backend connections managed by the Workspace.

### How does withWorkspace handle Workspace initialization errors?

The `__getWorkspaceStub()` method awaits `ws.ready()` before returning the stub. If Workspace initialization fails (for example, if the SQLite database cannot open or backend configuration is invalid), the promise rejects and propagates back through the RPC layer, preventing the caller from receiving an invalid or partially initialized stub.