> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deep.space/llms.txt
> Use this file to discover all available pages before exploring further.

# Rooms reference

> Durable Object base classes - RecordRoom, YjsRoom, CanvasRoom, PresenceRoom, CronRoom, GameRoom, JobRoom.

The SDK ships seven Durable Object base classes. Subclass them in `worker.ts`, declare them in `__DO_MANIFEST__`, and the SDK handles WebSocket upgrades, RBAC, persistence, and broadcast.

```ts theme={null}
import {
  BaseRoom, RecordRoom, YjsRoom, CanvasRoom,
  PresenceRoom, CronRoom, GameRoom, JobRoom,
} from 'deepspace/worker'
import type {
  RecordRoomConfig, CronRoomConfig, GameRoomConfig,
  CronTask, CronExecution, CanvasShape, Viewport,
  PresencePeer, Player, GameInput, UserAttachment,
  Job, JobContext,
} from 'deepspace/worker'
```

Each base class is parameterized over your `Env` interface so `this.env.<binding>` is typed inside overrides.

## `BaseRoom<E>`

Abstract parent of all rooms. Provides WebSocket plumbing, JWT identity parsing, and the connection lifecycle. Subclass directly only when none of the specialized rooms fit - rare in practice.

```ts theme={null}
abstract class BaseRoom<E = Record<string, unknown>> {
  constructor(state: DurableObjectState, env: unknown)

  // Lifecycle hooks subclasses override
  protected onConnect(
    ws: WebSocket,
    user: UserAttachment,
  ): UserAttachment | void | Promise<UserAttachment | void>

  protected abstract onMessage(
    ws: WebSocket,
    user: UserAttachment,
    message: { type: string; [key: string]: unknown },
  ): void | Promise<void>

  protected onBinaryMessage?(
    ws: WebSocket,
    user: UserAttachment,
    data: ArrayBuffer,
  ): void | Promise<void>

  protected onDisconnect(
    ws: WebSocket,
    user: UserAttachment,
  ): void | Promise<void>

  protected onRequest?(request: Request): Response | Promise<Response>
  protected onAlarm?(): void | Promise<void>
}

interface UserAttachment {
  userId: string
  userName: string
  userEmail: string
  userImageUrl?: string
  // Subclass-specific data is serialized alongside user info
  [key: string]: unknown
}
```

`onConnect` may return an augmented `UserAttachment` - the returned value (or the default) is serialized on the WebSocket via `state.acceptWebSocket(...)` and survives DO hibernation.

## `RecordRoom<E>`

Primary data DO - backs every record collection your app declares.

```ts theme={null}
class RecordRoom<E = Record<string, unknown>> extends BaseRoom<E> {
  constructor(
    state: DurableObjectState,
    env: unknown,
    schemas?: CollectionSchema[],
    config?: RecordRoomConfig,
  )
}

interface RecordRoomConfig {
  /** User ID of the app owner. Automatically gets the `admin` role on connect. */
  ownerUserId?: string
}
```

Scaffold pattern:

```ts theme={null}
export class AppRecordRoom extends RecordRoom<Env> {
  constructor(state: DurableObjectState, env: Env) {
    super(state, env, schemas, { ownerUserId: env.OWNER_USER_ID })
  }
}
```

## `YjsRoom<E>`

Per-document collaborative state (Y.Text, Y.Map, Y.Array).

```ts theme={null}
class YjsRoom<E = Record<string, unknown>> extends BaseRoom<E> {
  constructor(state: DurableObjectState, env: unknown)
}
```

Connected to via `/ws/yjs/:docId`. The DO persists the full Yjs update as a single binary blob in SQLite.

## `CanvasRoom<E>`

Collaborative canvas - shapes and viewports.

```ts theme={null}
class CanvasRoom<E = Record<string, unknown>> extends BaseRoom<E> {
  constructor(state: DurableObjectState, env: unknown)
}

interface CanvasShape {
  id: string
  type: string
  x: number
  y: number
  width: number
  height: number
  rotation?: number
  props: Record<string, unknown>
  createdBy: string
  createdAt: string
  updatedAt: string
}

interface Viewport {
  userId: string
  x: number
  y: number
  width: number
  height: number
  zoom: number
}
```

Connected to via `/ws/canvas/:docId`.

## `PresenceRoom<E>`

Ephemeral peer state - cursors, typing indicators, viewports. **Not persisted.**

```ts theme={null}
class PresenceRoom<E = Record<string, unknown>> extends BaseRoom<E> {
  constructor(state: DurableObjectState, env: unknown)
}

interface PresencePeer {
  userId: string
  userName: string
  userEmail: string
  userImageUrl?: string
  joinedAt: string
  /** Arbitrary per-user state (cursor, typing, viewport, etc.) */
  state: Record<string, unknown>
}
```

Connected to via `/ws/presence/:scopeId`.

## `CronRoom<E>`

Scheduled-task DO. Declare tasks in the constructor config and override `onTask`.

```ts theme={null}
abstract class CronRoom<E = Record<string, unknown>> extends BaseRoom<E> {
  constructor(state: DurableObjectState, env: unknown, config: CronRoomConfig)
  protected abstract onTask(taskName: string): void | Promise<void>
}

interface CronRoomConfig {
  tasks: CronTask[]
}

interface CronTask {
  name: string
  /** Interval in minutes - mutually exclusive with `schedule`. */
  intervalMinutes?: number
  /** 5-field cron expression - requires `timezone`. */
  schedule?: string
  /** IANA timezone string (e.g. "America/New_York"). Required with `schedule`. */
  timezone?: string
  /** Whether the task starts paused. */
  paused?: boolean
}

interface CronExecution {
  taskName: string
  startedAt: string
  /** Null while the task is still running. */
  completedAt: string | null
  success: boolean
  durationMs: number
  error?: string
}
```

Scaffold pattern:

```ts theme={null}
export class AppCronRoom extends CronRoom<Env> {
  constructor(state: DurableObjectState, env: Env) {
    super(state, env, { tasks: cronTasks })
  }
  protected async onTask(name: string) {
    await runCronTask(name, this.env)
  }
}
```

Connected to via `/ws/cron/:roomId` (admin/monitor stream).

## `GameRoom<E>`

Authoritative tick-based game loop DO.

```ts theme={null}
abstract class GameRoom<E = Record<string, unknown>> extends BaseRoom<E> {
  constructor(state: DurableObjectState, env: unknown, config?: GameRoomConfig)

  protected abstract onTick(
    state: Record<string, unknown>,
    inputs: GameInput[],
    tick: number,
  ): Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined>

  protected onPlayerJoin(player: Player): void
  protected onPlayerLeave(player: Player): void
  protected onGameStart(): void
  protected onGameEnd(finalState: Record<string, unknown>): void

  /** Override to migrate persisted state on schema bumps. */
  protected onHydrateState(stored: Record<string, unknown>): Record<string, unknown>
}

interface GameRoomConfig {
  /** Ticks per second (default: 20) */
  tickRate?: number
  /** Minimum players to start (default: 1) */
  minPlayers?: number
  /** Maximum players (default: unlimited) */
  maxPlayers?: number
}

interface Player {
  userId: string
  userName: string
  ready: boolean
  connectedAt: string
  data: Record<string, unknown>
}

interface GameInput {
  userId: string
  action: string
  data: Record<string, unknown>
  tick: number
}
```

Connected to via `/ws/game/:roomId`.

## `JobRoom<E>`

Durable background-job DO. Handlers run on the DO's alarm with a \~15-minute wall budget per tick (chain longer jobs with `ctx.continue(state)`). Jobs are persisted in SQLite, survive isolate restarts, and broadcast every state change over WebSocket - clients see live progress without polling. For worked patterns, see the [Background jobs guide](/guides/background-jobs).

```ts theme={null}
abstract class JobRoom<
  E = Record<string, unknown>,
  P = unknown,
  R = unknown,
> extends BaseRoom<E> {
  constructor(state: DurableObjectState, env: unknown, config?: JobRoomConfig)
  protected abstract onJob(job: Job<P>, ctx: JobContext): R | void | Promise<R | void>
}

interface JobRoomConfig {
  /** Default retry budget for jobs that don't pass `maxAttempts` to `enqueue`. Default 1 (no auto-retry). */
  defaultMaxAttempts?: number
  /** How long terminal rows (succeeded / failed / canceled) are kept. Default 24h. */
  retentionMs?: number
  /** Default retry backoff for failed jobs. Default 1000ms. */
  retryBackoffMs?: number
  /** How many historical rows the DO holds in its in-memory snapshot before evicting. Default 100. */
  snapshotLimit?: number
}

interface Job {
  id: string
  type: string
  payload?: unknown
  status: 'queued' | 'running' | 'succeeded' | 'failed' | 'canceled'
  attempts: number
  maxAttempts: number
  /** Return value of a successful `onJob`. */
  result?: unknown
  /** Error message from a thrown `onJob`. */
  error?: string
  /** Last `ctx.progress(value)` value (0..1). Present while live. */
  progress?: number
  /** Last `ctx.progress(_, message)` string. */
  progressMessage?: string
  enqueuedAt: string
  startedAt?: string | null
  completedAt?: string | null
  enqueuedBy?: string | null
  /** State checkpoint from a previous `ctx.continue(state)` call. */
  resumeFrom?: unknown
}

interface JobContext {
  /** Broadcast progress (0..1) and an optional human-readable message. */
  progress(value: number, message?: string): void
  /** AbortSignal that fires on client cancel. Pass to `fetch(url, { signal })`. */
  signal: AbortSignal
  /** Checkpoint state and yield to the next alarm tick. Call this then `return`. */
  continue(state: unknown, opts?: { afterMs?: number }): void
}
```

The `onJob` return value becomes `job.result` (must be JSON-serializable); throwing fails the job and triggers a retry if `attempts < maxAttempts`. There is no `ctx.complete()` / `ctx.fail()` - return or throw. Sync returns are allowed - `onJob` does not have to be `async`.

`P` and `R` narrow the job payload and result types: `JobRoom<Env, MyPayload, MyResult>` gives you `onJob(job: Job<MyPayload>, ctx): MyResult | void | Promise<MyResult | void>`. Most apps leave them as `unknown` and cast at the dispatch site (see the scaffold pattern below).

Scaffold pattern:

```ts theme={null}
export class AppJobRoom extends JobRoom<Env> {
  constructor(state: DurableObjectState, env: Env) {
    super(state, env)
  }
  protected async onJob(job: Job, ctx: JobContext): Promise<unknown> {
    return await runJob(job, ctx, this.env)
  }
}
```

Connected to via `/ws/jobs/:roomId`. For the worker-side enqueue helper that lets you enqueue from HTTP routes, cron handlers, or server actions (different isolate from the DO), see [`enqueueJob`](#enqueuejob-namespace-roomid-type-payload-opts) below.

### `enqueueJob(namespace, roomId, type, payload?, opts?)`

Cross-isolate enqueue helper - use anywhere outside the JobRoom DO (HTTP routes, cron handlers, server actions, AI routes).

```ts theme={null}
function enqueueJob(
  namespace: DurableObjectNamespace,
  roomId: string,
  type: string,
  payload?: unknown,
  opts?: { maxAttempts?: number; enqueuedBy?: string },
): Promise<string>   // resolves with the jobId
```

Pass `app:${env.APP_NAME}` as `roomId` to hit the per-app `AppJobRoom`. From inside an `onJob` handler, call `this.enqueue(...)` directly instead - it skips the HTTP hop.

> Audio/video rooms have no SDK DO class. Use LiveKit via the `livekit/*` integration endpoints instead.

## The DO manifest

```ts theme={null}
import type { DOManifest, DOManifestEntry, DOBindings } from 'deepspace/worker'
```

| Export                               | Type  | Description                                                                                          |
| ------------------------------------ | ----- | ---------------------------------------------------------------------------------------------------- |
| `DOManifest`                         | type  | `DOManifestEntry[]` - shape of `__DO_MANIFEST__`.                                                    |
| `DOManifestEntry`                    | type  | `{ binding: string; className: string; sqlite: boolean }`.                                           |
| `DOBindings<typeof __DO_MANIFEST__>` | type  | Derives the `Env` interface's DO bindings from the manifest.                                         |
| `DEFAULT_DO_MANIFEST`                | const | Two-entry fallback (`RECORD_ROOMS` + `YJS_ROOMS`) used when an app doesn't export `__DO_MANIFEST__`. |

The scaffold's pattern:

```ts theme={null}
export const __DO_MANIFEST__ = [
  { binding: 'RECORD_ROOMS',   className: 'AppRecordRoom',   sqlite: true },
  { binding: 'YJS_ROOMS',      className: 'AppYjsRoom',      sqlite: true },
  { binding: 'CANVAS_ROOMS',   className: 'AppCanvasRoom',   sqlite: true },
  { binding: 'PRESENCE_ROOMS', className: 'AppPresenceRoom', sqlite: true },
  { binding: 'CRON_ROOMS',     className: 'AppCronRoom',     sqlite: true },
  { binding: 'JOB_ROOMS',      className: 'AppJobRoom',      sqlite: true },
] as const satisfies DOManifest

interface Env extends DOBindings<typeof __DO_MANIFEST__> {
  // ...secrets and custom bindings
}
```

## See also

* [Architecture concepts](/concepts/architecture) - how DOs fit into the system
* [Get started → Project structure](/get-started/project-structure) - the DO manifest in context
* [Cron guide](/guides/scheduled-jobs) - patterns for `CronRoom`
