> ## 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.

# Real-time reference

> Presence, Yjs, canvas, game rooms, and cursor color helpers.

The real-time hooks beyond records and messaging - presence, collaborative editing, canvas, game rooms, and helpers.

```ts theme={null}
import {
  // Yjs
  useYjsText, useYjsField, useYjsRoom,
  // Canvas
  useCanvas,
  // Presence
  usePresence, usePresenceRoom,
  // Game rooms
  useGameRoom,
  // Cron monitor
  useCronMonitor,
  // Background jobs
  useJobs,
  // User colors
  DEFAULT_USER_COLORS, getUserColor,
  // Low-level sync primitives
  createEncoder, createDecoder, encodeSyncStep1, encodeSyncStep2, encodeUpdate,
  handleSyncMessage, Awareness, encodeAwarenessMessage, handleAwarenessMessage,
} from 'deepspace'
```

## Yjs hooks

### `useYjsText(collection, recordId, fieldName)`

Collaborative plain text bound to a record field.

```ts theme={null}
function useYjsText(
  collection: string,
  recordId: string,
  fieldName: string,
): {
  text: string
  setText: (value: string) => void
  synced: boolean
  canWrite: boolean
}
```

### `useYjsField(collection, recordId, fieldName)`

Lower-level Yjs binding. Returns the raw `Y.Doc` and `Awareness` so you can build any Yjs type (`Y.Map`, `Y.Array`, `Y.XmlFragment`, etc.) on top of it. Not generic - the hook doesn't model the field shape.

```ts theme={null}
function useYjsField(
  collection: string,
  recordId: string,
  fieldName: string,
): {
  doc: Y.Doc
  awareness: Awareness
  synced: boolean
  canWrite: boolean
  /** Increments on every local or remote Y.Doc update - useful as a render trigger. */
  updateCount: number
}
```

Build whichever Yjs type you need off `doc`:

```tsx theme={null}
const { doc, synced } = useYjsField('documents', docId, 'tasks')
const list = useMemo(() => doc.getArray<Task>('tasks'), [doc])
```

### `useYjsRoom(docId, fieldName)`

Standalone Yjs document not tied to a record. Useful for ephemeral collaboration sessions. Opens a direct WebSocket to a dedicated `YjsRoom` DO at `/ws/yjs/:docId`.

```ts theme={null}
function useYjsRoom(docId: string, fieldName: string): {
  doc: Y.Doc
  awareness: Awareness
  text: string
  setText: (value: string) => void
  synced: boolean
  canWrite: boolean
}
```

`text` / `setText` are bound to the `Y.Text` at `fieldName`. Use `doc` directly if you need a different type.

`awareness` is a [y-protocols `Awareness`](https://github.com/yjs/y-protocols) instance pre-wired to the same WebSocket. Calls to `awareness.setLocalState(...)` or `awareness.setLocalStateField('cursor' | 'selection' | 'user' | …, value)` fire an `MSG_AWARENESS` frame to peers; remote states arrive on the awareness `'change'` / `'update'` events and are visible via `awareness.getStates()`. Pass it to an editor binding (e.g. `@tiptap/extension-collaboration-cursor`) or wire your own cursor/selection UI - see the [collaborative editing guide](/guides/collaborative-editing#cursors-and-selections) for a worked example.

<Note>
  The `/ws/yjs/:docId` route is token-required and docs-aware: 401 without a verified JWT, 403 without read access. With the docs feature installed, roles resolve from `documents.ownerId` / `editors` / `collaborators`; without it, any authenticated caller is treated as `member`. See the [security model](/concepts/architecture#security-model-websocket-identity) for the full path.
</Note>

For the raw protocol constants (`MSG_AWARENESS`, `encodeAwarenessMessage`, `handleAwarenessMessage`) used by this hook internally, see [Low-level sync primitives](#low-level-sync-primitives) below.

## `useCanvas(roomId)`

Connects to a `CanvasRoom` DO.

```ts theme={null}
function useCanvas(roomId: string): {
  shapes: CanvasShapeClient[]
  /** All connected users' viewports (including self). */
  viewports: ViewportClient[]
  connected: boolean
  /** RBAC gate. False until the server AUTH frame lands; stays false for viewers. */
  canWrite: boolean
  addShape:    (shape: Partial<CanvasShapeClient>) => void
  moveShape:   (shapeId: string, x: number, y: number) => void
  resizeShape: (shapeId: string, width: number, height: number, x?: number, y?: number) => void
  updateShape: (shapeId: string, props: Record<string, unknown>) => void
  deleteShape: (shapeId: string) => void
  setViewport: (viewport: Omit<ViewportClient, 'userId'>) => void
  undo:        () => void
  redo:        () => void
}
```

All mutation methods are fire-and-forget - they encode and send a typed message and return `void`. `viewports` is an array, not a `Map`.

Use `canWrite` to disable shape/draw controls for viewers - shape mutations (`addShape`, `moveShape`, `resizeShape`, `updateShape`, `deleteShape`, `undo`, `redo`) **silently no-op when `canWrite` is `false`**. `setViewport` is exempt and stays open for viewers (viewport broadcasts are presence-like, not stateful writes).

Each shape:

```ts theme={null}
type CanvasShapeClient = {
  id: string
  type: string                     // free-form: 'rect', 'circle', 'text', etc.
  x: number
  y: number
  width: number
  height: number
  rotation?: number
  props: Record<string, unknown>   // app-specific payload
  createdBy: string
  createdAt: string
  updatedAt: string
}

type ViewportClient = {
  userId: string
  x: number
  y: number
  width: number
  height: number
  zoom: number
}
```

## Presence hooks

### `usePresence(options?)`

Online/offline status derived from `lastSeenAt` heartbeats on the users collection.

```ts theme={null}
function usePresence(options?: { timeoutMs?: number }): {
  users: RoomUser[]
  isOnline:    (userId: string) => boolean
  getLastSeen: (userId: string) => string | null
}
```

The hook also sends a heartbeat every 60 seconds so the server refreshes the caller's `lastSeenAt`. Default `timeoutMs` is 5 minutes.

### `usePresenceRoom(scopeId)`

High-frequency ephemeral state (cursors, typing, viewport). Connects to a dedicated `PresenceRoom` DO.

```ts theme={null}
function usePresenceRoom(scopeId: string): {
  peers: PresencePeerClient[]   // excludes self
  connected: boolean
  updateState: (state: object) => void   // merges
}

type PresencePeerClient = {
  userId: string
  userName: string
  userEmail: string
  userImageUrl?: string
  joinedAt: string
  state: Record<string, unknown>
}
```

`scopeId` is any string. Common patterns: `canvas:${canvasId}`, `thread:${channelId}`, `doc:${docId}`.

## `useGameRoom(roomId)`

Turn-tick or sim-tick game loop DO. Not generic - `state` is `Record<string, unknown>` (cast at the use-site).

```ts theme={null}
function useGameRoom(roomId: string): {
  state: Record<string, unknown>
  tick: number
  players: GamePlayer[]
  running: boolean
  connected: boolean
  /** RBAC gate. False until the server AUTH frame lands; stays false for spectators. */
  canWrite: boolean
  sendInput:  (action: string, data?: Record<string, unknown>) => void
  setReady:   () => void
  startGame:  () => void
  endGame:    () => void
}

type GamePlayer = {
  userId: string
  userName: string
  ready: boolean
  connectedAt: string
  /** Server-managed per-player slot - always present, never undefined. */
  data: Record<string, unknown>
}
```

All mutation methods (`sendInput`, `setReady`, `startGame`, `endGame`) are fire-and-forget - they return `void` - and **silently no-op when `canWrite` is `false`**. Use `canWrite` to disable lobby and input UI for spectators. The server-side `GameInput` event carries `userId` (not `playerId`), `action`, `data`, and the current `tick`.

State migration on schema bumps lives in the worker - override `onHydrateState(stored)` on your `GameRoom` subclass.

## `useCronMonitor(roomId)`

Admin/monitor stream for the `CronRoom` DO. Pass `app:<APP_NAME>` for the app's default cron room.

```ts theme={null}
function useCronMonitor(roomId: string): {
  tasks: CronTaskState[]
  history: CronHistoryEntry[]
  connected: boolean
  /** RBAC gate. False until the server AUTH frame lands; stays false for read-only viewers. */
  canWrite: boolean
  trigger: (taskName: string) => void
  pause:   (taskName: string) => void
  resume:  (taskName: string) => void
}

type CronTaskState = {
  name: string
  /** Required. Null when the task is configured via `schedule` instead. */
  intervalMinutes: number | null
  /** Required. Null when the task is configured via `intervalMinutes` instead. */
  schedule: string | null
  /** Required. Null when the task has no explicit timezone. */
  timezone: string | null
  paused: boolean
  lastRunAt: string | null
  nextRunAt: string | null
}

type CronHistoryEntry = {
  taskName: string
  startedAt: string
  completedAt: string | null
  success: boolean
  durationMs: number
  error?: string
}
```

`trigger`, `pause`, and `resume` are fire-and-forget - they return `void` - and **silently no-op when `canWrite` is `false`**.

<Warning>
  **Default scaffold lets every signed-in user fire owner-billed tasks.** The Cron DO authorizes `trigger` / `pause` / `resume` based on the role the `wsRoute` resolver returns. The scaffolded `/ws/cron/:roomId` returns `role: 'member'` for any signed-in connection, so by default **any signed-in user gets `canWrite: true`**. For owner-only writes, replace the scaffolded `wsRoute` helper with an inline handler that returns `role: 'member'` only when the JWT subject matches `OWNER_USER_ID`. Then also disable the controls client-side by `canWrite` (or `useUser().user?.role === 'admin'`) so the buttons match server policy.
</Warning>

## `useJobs(roomId)`

Enqueue and monitor durable background jobs on the `JobRoom` DO. Pass `app:<APP_NAME>` for the per-app job room. The hook subscribes via WebSocket, so `jobs` re-renders on every state change - no polling.

```ts theme={null}
function useJobs(roomId: string): {
  jobs: JobView[]
  connected: boolean
  enqueue(
    type: string,
    payload?: unknown,
    opts?: { maxAttempts?: number },
  ): Promise<string>                       // resolves with the jobId
  getJob(id: string): JobView | undefined
  cancel(id: string): void
  retry(id: string): void
}

type JobView = {
  id: string
  type: string
  status: 'queued' | 'running' | 'succeeded' | 'failed' | 'canceled'
  payload?: unknown
  result?: unknown
  error?: string
  /** 0..1 - present while live. */
  progress?: number
  progressMessage?: string
  attempts: number
  maxAttempts: number
  enqueuedAt: string
  startedAt?: string
  completedAt?: string
  enqueuedBy?: string
}
```

`enqueue` resolves once the server acks (10 s timeout, also rejects on socket close). `cancel(id)` is best-effort - it always flips the DB row to `canceled`, and if the cancel reaches the isolate running the job, `ctx.signal` fires so any in-flight `fetch(...)` you wired with `signal: ctx.signal` aborts cleanly. `retry(id)` re-queues a failed or canceled job.

The handler runs in your `AppJobRoom`'s `onJob(job, ctx)` - see [Background jobs](/guides/background-jobs) for the worker-side pattern and the `enqueueJob` helper for HTTP routes / cron / actions.

<Warning>
  **The JobRoom DO does not enforce a role on `enqueue` / `cancel` / `retry`.** Unlike `useCronMonitor` - whose DO gates writes off the `wsRoute` resolver role - any signed-in (or anonymous) connection can enqueue jobs. For jobs that spend owner credits via integrations or AI proxies, gate the button by `useUser().user?.role === 'admin'` or wrap the page route in `(protected)/`. See [Background jobs](/guides/background-jobs#auth-gating-paid-jobs) for the pattern.
</Warning>

## User colors

```ts theme={null}
// 12-color palette of cursor/avatar tints
const DEFAULT_USER_COLORS: readonly string[]

// Deterministic hash: same userId → same color
function getUserColor(userId: string, palette?: string[]): string
```

Use for cursor dots in `usePresence` / `usePresenceRoom`, avatar fallbacks, and "who's typing" pills.

## Low-level sync primitives

For building custom hooks against a DeepSpace Yjs DO (rare):

```ts theme={null}
createEncoder, createDecoder
toUint8Array, writeVarUint, writeVarUint8Array, readVarUint, readVarUint8Array
encodeSyncStep1, encodeSyncStep2, encodeUpdate
handleSyncMessage
Awareness, encodeAwarenessMessage, handleAwarenessMessage
getMessageType
MSG_SYNC, MSG_AWARENESS, MSG_SYNC_STEP1, MSG_SYNC_STEP2, MSG_SYNC_UPDATE
```

Most apps never use these directly. Use the higher-level hooks (`useYjsText`, etc.) unless you're building a custom binding.

## See also

* [Collaborative editing guide](/guides/collaborative-editing)
* [Presence and cursors guide](/guides/presence-and-cursors)
* [Canvas guide](/guides/canvas)
* [Scheduled tasks guide](/guides/scheduled-jobs)
* [Background jobs guide](/guides/background-jobs)
