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'
| 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:
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