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

# Records reference

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

The records API is the primary surface for working with [collections](/concepts/data-model). Every hook and provider on this page is imported from `deepspace`.

```ts theme={null}
import {
  RecordProvider, RecordScope, ScopeRegistryProvider,
  useQuery, useMutations, useUsers, useUserLookup, useRecordContext,
} from 'deepspace'
```

For schemas and column types, see the [worker schemas reference](/sdk-reference/worker/schemas). For RBAC rules, see [permissions](/concepts/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](/guides/authentication).                    |
| `getAuthToken`   | `() => Promise<string \| null>` *(optional)*      | Custom token fetcher. Defaults to the SDK's.                                                                                         |

```tsx theme={null}
<RecordProvider allowAnonymous>
  <App />
</RecordProvider>
```

### `<RecordScope>`

Mounts a specific [scope](/concepts/architecture#scopes) (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](/concepts/architecture#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](/concepts/data-model#collections-and-records).

```ts theme={null}
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
}
```

<Tabs>
  <Tab title="Basic">
    Subscribe to every record in a collection. The hook re-renders whenever any user mutates a record visible to the caller's [permissions](/concepts/permissions).

    ```tsx theme={null}
    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>)
    }
    ```
  </Tab>

  <Tab title="Filter">
    `where` filters by exact field match. Filtering happens server-side before broadcast, so unauthorized records never leave the worker.

    ```tsx theme={null}
    const { records } = useQuery<Note>('notes', {
      where: { pinned: true },
    })
    ```
  </Tab>

  <Tab title="Sort & limit">
    `orderBy` accepts any field name including `createdAt` and `updatedAt`. `limit` caps the records sent over the wire.

    ```tsx theme={null}
    const { records } = useQuery<Note>('notes', {
      orderBy: 'updatedAt',
      orderDir: 'desc',
      limit: 50,
    })
    ```
  </Tab>

  <Tab title="Status">
    Gate your UI on `status` rather than `records.length`. An empty array can mean either "loading" or "loaded with no rows".

    ```tsx theme={null}
    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} />
    ```
  </Tab>
</Tabs>

Envelope shape:

```ts theme={null}
type Envelope<T> = {
  recordId: string
  data: T
  createdBy: string
  createdAt: string
  updatedAt: string
}
```

<Warning>
  **User fields live under `.data`.** `r.title` returns `undefined` - always reach for `r.data.title`. TypeScript catches this if you pass a row type to `useQuery<T>`.
</Warning>

## `useMutations<T>(collection)`

Returns mutation functions for the given collection. Each mutation applies [optimistically](/concepts/realtime-sync#the-optimistic-mutation-pipeline) - the local store updates before the server confirms.

```ts theme={null}
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>
}
```

<Tabs>
  <Tab title="Create">
    `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.

    ```tsx theme={null}
    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:

    ```tsx theme={null}
    const id = await createConfirmed({ title: 'New', body: '', pinned: false })
    navigate(`/notes/${id}`)
    ```
  </Tab>

  <Tab title="Update">
    `put` is **merge semantics** - the server applies `{ ...existing, ...patch }`. Send only the fields you're changing.

    ```tsx theme={null}
    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:

    ```tsx theme={null}
    // ❌ Wasteful - sends every field
    await put(noteId, { ...note.data, pinned: true })

    // ✅ Send only what changed
    await put(noteId, { pinned: true })
    ```
  </Tab>

  <Tab title="Delete">
    `remove` is a hard delete. The record is dropped from the DO's SQLite store and broadcast as `record_removed` to every connected client.

    ```tsx theme={null}
    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.
  </Tab>

  <Tab title="Confirmed variants">
    `createConfirmed` / `putConfirmed` / `removeConfirmed` resolve only after the DO has acknowledged the write. Use when the next step depends on server persistence - typically before navigation.

    ```tsx theme={null}
    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).
  </Tab>
</Tabs>

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

```ts theme={null}
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](/concepts/permissions#roles) for how roles map onto collection RBAC rules.

## `useUserLookup()`

O(1) wrapper around `useUsers()` for resolving `userId`s to display fields.

```ts theme={null}
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
}
```

```tsx theme={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

* [Data storage guide](/guides/data-storage) - schemas, CRUD, and patterns.
* [Data model](/concepts/data-model) - collections, envelopes, scopes.
* [Permissions](/concepts/permissions) - RBAC rules and the `'own'` / `'shared'` / `'published'` shortcuts.
* [Real-time sync](/concepts/realtime-sync) - optimistic pipeline and consistency guarantees.
* [Worker schemas reference](/sdk-reference/worker/schemas) - the `CollectionSchema` type and drop-in collections.
