Skip to main content

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.

The SDK ships six Durable Object base classes. Subclass them in worker.ts, declare them in __DO_MANIFEST__, and the SDK handles WebSocket upgrades, RBAC, persistence, and broadcast.
import {
  BaseRoom, RecordRoom, YjsRoom, CanvasRoom,
  PresenceRoom, CronRoom, GameRoom,
} from 'deepspace/worker'
import type {
  RecordRoomConfig, CronRoomConfig, GameRoomConfig,
  CronTask, CronExecution, CanvasShape, Viewport,
  PresencePeer, Player, GameInput, UserAttachment,
} 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.
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.
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:
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).
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.
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.
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.
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:
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.
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.
Audio/video rooms have no SDK DO class. Use LiveKit via the livekit/* integration endpoints instead.

The DO manifest

import type { DOManifest, DOManifestEntry, DOBindings } from 'deepspace/worker'
ExportTypeDescription
DOManifesttypeDOManifestEntry[] - shape of __DO_MANIFEST__.
DOManifestEntrytype{ binding: string; className: string; sqlite: boolean }.
DOBindings<typeof __DO_MANIFEST__>typeDerives the Env interface’s DO bindings from the manifest.
DEFAULT_DO_MANIFESTconstTwo-entry fallback (RECORD_ROOMS + YJS_ROOMS) used when an app doesn’t export __DO_MANIFEST__.
The scaffold’s pattern:
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 },
] as const satisfies DOManifest

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

See also