Skip to main content
Docs
SDK reference

Records reference

Providers, hooks, and the data layer for reading and writing records.

The records API is the primary surface for working with collections. Every hook and provider on this page is imported from deepspace.

import {
  RecordProvider, RecordScope, ScopeRegistryProvider,
  useQuery, useMutations, useUsers, useUserLookup, useRecordContext,
} from 'deepspace'

For schemas and column types, see the worker schemas reference. For RBAC rules, see permissions.

Providers#

<RecordProvider>#

Initializes the WebSocket and in-memory record store. Required ancestor of every records hook.

Prop Type Description
roomId string (optional) Scope ID for the default room (usually app:<APP_NAME>). Omit for multi-scope mode and use <RecordScope> to mount scopes instead.
schemas CollectionSchema[] (optional) All collections this provider tree may query.
wsUrl string (optional) Override the WebSocket URL. Defaults to current origin.
fetchUser () => Promise<UserProfile \| null> (optional) Custom user-profile fetcher. Defaults to using the Better Auth session.
allowAnonymous boolean (optional) Connect without a JWT (default false). Required for public pages. See authentication.
getAuthToken () => Promise<string \| null> (optional) Custom token fetcher. Defaults to the SDK's.
<RecordProvider allowAnonymous>
  <App />
</RecordProvider>

<RecordScope>#

Mounts a specific scope (Durable Object instance). Nest for additional scopes.

Prop Type Description
roomId string Scope ID (e.g. app:my-app, conv:abc123).
schemas CollectionSchema[] Collections in this scope.
appId string App identifier - used for cross-app routing.
sharedScopes Array<{ roomId, schemas }> Cross-app scopes to mount alongside the primary. See cross-app shared scopes.
wsUrl string (optional) Override WebSocket URL.
wsPathPrefix string (optional) Override path prefix (default /ws).
isolated boolean If true, this scope's store is independent of parent stores.

<ScopeRegistryProvider>#

Required once near the root if your app uses shared scopes via sharedScopes. Coordinates routing between cross-app and per-app DOs.

useQuery<T>(collection, options?)#

Subscribes to a collection. Returns a reactive array of envelopes.

function useQuery<T>(
  collection: string,
  options?: {
    where?: Partial<T>
    orderBy?: string
    orderDir?: 'asc' | 'desc'
    limit?: number
  },
): {
  records: Envelope<T>[]
  status: 'loading' | 'ready' | 'error'
  error?: string
}

Subscribe to every record in a collection. The hook re-renders whenever any user mutates a record visible to the caller's permissions.

type Note = { title: string; body: string }

function Notes() {
  const { records, status } = useQuery<Note>('notes')

  if (status === 'loading') return <Skeleton />
  return records.map((r) => <li key={r.recordId}>{r.data.title}</li>)
}
where filters by exact field match. Filtering happens server-side before broadcast, so unauthorized records never leave the worker.

const { records } = useQuery<Note>('notes', {
  where: { pinned: true },
})
orderBy accepts any field name including createdAt and updatedAt. limit caps the records sent over the wire.

const { records } = useQuery<Note>('notes', {
  orderBy: 'updatedAt',
  orderDir: 'desc',
  limit: 50,
})

Gate your UI on status rather than records.length. An empty array can mean either "loading" or "loaded with no rows".

const { records, status, error } = useQuery<Note>('notes')

if (status === 'loading') return <SkeletonList />
if (status === 'error') return <ErrorBanner message={error} />
if (records.length === 0) return <EmptyState />
return <NoteList notes={records} />

Envelope shape:

type Envelope<T> = {
  recordId: string
  data: T
  createdBy: string
  createdAt: string
  updatedAt: string
}

useMutations<T>(collection)#

Returns mutation functions for the given collection. Each mutation applies optimistically - the local store updates before the server confirms.

function useMutations<T>(collection: string): {
  create:          (data: T)                       => Promise<string>
  put:             (id: string, patch: Partial<T>) => Promise<void>
  remove:          (id: string)                    => Promise<void>
  createConfirmed: (data: T)                       => Promise<string>
  putConfirmed:    (id: string, patch: Partial<T>) => Promise<void>
  removeConfirmed: (id: string)                    => Promise<void>
}
create takes the full row shape and returns the new recordId. The ID is generated on the client (timestamp + random suffix) before the write is sent, so the promise resolves with the ID immediately while the server processes the mutation in the background.

const { create } = useMutations<Note>('notes')

const id = await create({
  title: 'Untitled',
  body: '',
  pinned: false,
})
If you need to confirm the row was actually persisted (e.g., before navigating away), use createConfirmed instead - it awaits server acknowledgment:
const id = await createConfirmed({ title: 'New', body: '', pinned: false })
navigate(`/notes/${id}`)
put is merge semantics - the server applies { ...existing, ...patch }. Send only the fields you're changing.

const { put } = useMutations<Note>('notes')

await put(noteId, { pinned: true })          // only updates pinned
await put(noteId, { title: 'New title' })    // only updates title
Do not spread the existing record:
// ❌ Wasteful - sends every field
await put(noteId, { ...note.data, pinned: true })

// ✅ Send only what changed
await put(noteId, { pinned: true })
remove is a hard delete. The record is dropped from the DO's SQLite store and broadcast as record_removed to every connected client.

const { remove } = useMutations<Note>('notes')

await remove(noteId)
There is no soft-delete primitive at the records layer. For chat messages, use [useMessages().softDelete](/sdk-reference/client/messaging#usemessages) which sets a tombstone flag instead.
createConfirmed / putConfirmed / removeConfirmed resolve only after the DO has acknowledged the write. Use when the next step depends on server persistence - typically before navigation.

const { createConfirmed } = useMutations<Note>('notes')

const id = await createConfirmed({ title: 'New' })
navigate(`/notes/${id}`)   // safe - server has persisted
Plain create resolves as soon as the local store is updated, which is usually before the server confirms. If the server then rejects the write (e.g., RBAC denial), the optimistic update [rolls back](/concepts/realtime-sync#the-optimistic-mutation-pipeline).
Method Semantics Returns
create Optimistic Promise<string> (client-generated recordId)
put Optimistic, merge Promise<void>
remove Optimistic Promise<void>
createConfirmed Waits for DO ack Promise<string>
putConfirmed Waits for DO ack Promise<void>
removeConfirmed Waits for DO ack Promise<void>

useUsers()#

Returns the users collection with role-management helpers.

type RoomUser = {
  id: string
  email: string
  name: string
  imageUrl?: string
  role: string
  createdAt: string
  lastSeenAt: string
}

function useUsers(): {
  users: RoomUser[]
  usersLoaded: boolean
  setRole: (userId: string, role: string) => void
  refresh: () => void
}
setRole accepts a free-form role string (e.g. 'admin', 'intern', or any value your schema understands) and dispatches the change without waiting for an ack. The Durable Object enforces who is allowed to call it. See permissions for how roles map onto collection RBAC rules.

useUserLookup()#

O(1) wrapper around useUsers() for resolving userIds to display fields.

type UserInfo = {
  id: string
  email: string
  name: string
  imageUrl?: string
  role: string
}

function useUserLookup(): {
  users: RoomUser[]
  usersLoaded: boolean
  userMap: Map<string, UserInfo>
  getUser:  (userId: string) => UserInfo | null
  getEmail: (userId: string) => string | null
  getName:  (userId: string) => string | null
}
const { getName } = useUserLookup()
<p>By {getName(message.authorId) ?? 'unknown'}</p>

There is no getRole or getImageUrl - read those off getUser(id)?.role or getUser(id)?.imageUrl.

useRecordContext()#

Low-level access to the record-store context (WebSocket send/receive primitives, ready state, user profile, etc.). Useful for building custom hooks or imperative reads outside React's render cycle. Most apps never need this.

See also#