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

# Data storage

> Define a collection, query it from React, and mutate it with optimistic updates.

This guide walks through adding a new [collection](/concepts/data-model) to your app: declaring its [schema](/sdk-reference/worker/schemas), querying it from the client, and persisting writes. By the end, you'll have a working CRUD page backed by a [Durable Object](/concepts/architecture#durable-objects) that syncs in real time across every connected client.

For background on envelopes, scopes, and the optimistic pipeline, see [Data model](/concepts/data-model) and [Real-time sync](/concepts/realtime-sync).

## Define the schema

Schemas live under `src/schemas/`, one file per collection. Create a new collection called `notes`:

```ts theme={null}
// src/schemas/notes-schema.ts
import type { CollectionSchema } from 'deepspace/worker'

export const notesSchema: CollectionSchema = {
  name: 'notes',
  columns: [
    { name: 'title', storage: 'text', interpretation: 'plain' },
    { name: 'body', storage: 'text', interpretation: 'plain' },
    { name: 'pinned', storage: 'number', interpretation: { kind: 'boolean' } },
  ],
  permissions: {
    member: { read: true, create: true, update: 'own', delete: 'own' },
    admin:  { read: true, create: true, update: true, delete: true },
  },
}
```

Register it in `src/schemas.ts`:

```ts theme={null}
import type { CollectionSchema } from 'deepspace/worker'
import { usersSchema } from './schemas/users-schema'
import { settingsSchema } from './schemas/admin-schema'
import { notesSchema } from './schemas/notes-schema'

export const schemas: CollectionSchema[] = [usersSchema, settingsSchema, notesSchema]
```

Restart `npx deepspace dev` if it's running - schemas are picked up at worker startup.

<Note>
  Schemas are baked into your worker at deploy time. There is no runtime schema registry; to add or change a schema in production, redeploy.
</Note>

## Read records

[`useQuery`](/sdk-reference/client/records#usequery\<t>-collection-options) subscribes to a collection and streams updates over a WebSocket. Each record arrives as an envelope, with your fields under `.data`:

```tsx theme={null}
import { useQuery } from 'deepspace'

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

function NotesList() {
  const { records, status, error } = useQuery<Note>('notes', {
    orderBy: 'createdAt',
    orderDir: 'desc',
  })

  if (status === 'loading') return <p>Loading…</p>
  if (status === 'error') return <p>Error: {error}</p>

  return (
    <ul>
      {records.map((note) => (
        <li key={note.recordId}>
          <h3>{note.data.title}</h3>
          <p>{note.data.body}</p>
        </li>
      ))}
    </ul>
  )
}
```

<Warning>
  User fields live under `.data`. Reading `note.title` returns `undefined` - always use `note.data.title`. TypeScript catches this if you pass a row type to `useQuery<Note>`.
</Warning>

For filtering, sorting, and limit options, see [`useQuery` options](/sdk-reference/client/records#usequery\<t>-collection-options). The Durable Object applies `where` server-side before broadcasting, so unauthorized records never leave the worker.

## Write records

[`useMutations`](/sdk-reference/client/records#usemutations\<t>-collection) returns `create` / `put` / `remove` plus `*Confirmed` variants. Each mutation applies [optimistically](/concepts/realtime-sync#the-mutation-pipeline) - the local store updates before the server confirms.

```tsx theme={null}
import { useMutations } from 'deepspace'

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

const id = await create({ title: 'Untitled', body: '', pinned: false })
await put(id, { pinned: true })          // merge: only updates pinned
await remove(id)
```

* **`create` returns the `recordId` immediately.** The ID is generated client-side (timestamp + random suffix) before the write is sent, so you can navigate to `/notes/${id}` without waiting for the server.
* **`put` is merge semantics.** The server applies `{ ...existing, ...patch }`. Send only the fields you're changing - don't spread the whole row.
* **`remove` is a hard delete.** There's no soft-delete primitive at the records layer.
* **Use `createConfirmed` when persistence matters before the next step.** Plain `create` resolves on local update; if the server later rejects (RBAC, validation), the optimistic write rolls back. `createConfirmed` waits for the Durable Object ack and rejects the promise on server denial.

See the [reference](/sdk-reference/client/records) for the full method table.

## A complete CRUD example

```tsx theme={null}
import { useState } from 'react'
import { useQuery, useMutations } from 'deepspace'
import { useToast } from '../components/ui'

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

export default function NotesPage() {
  const { records, status } = useQuery<Note>('notes', { orderBy: 'createdAt', orderDir: 'desc' })
  const { create, put, remove } = useMutations<Note>('notes')
  const { success, error } = useToast()
  const [draft, setDraft] = useState('')

  async function addNote() {
    if (!draft.trim()) return
    try {
      await create({ title: draft, body: '', pinned: false })
      setDraft('')
      success('Note created')
    } catch (e) {
      error('Could not create note', String(e))
    }
  }

  if (status === 'loading') return <p>Loading…</p>

  return (
    <div>
      <input value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="New note…" />
      <button onClick={addNote}>Add</button>
      <ul>
        {records.map((note) => (
          <li key={note.recordId}>
            <input
              type="checkbox"
              checked={note.data.pinned}
              onChange={(e) => put(note.recordId, { pinned: e.target.checked })}
            />
            {note.data.title}
            <button onClick={() => remove(note.recordId)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  )
}
```

Open the page in two browser windows - changes in one window appear in the other in real time.

## JSON columns

For structured field data, use `interpretation: { kind: 'json' }`:

```ts theme={null}
{ name: 'tags', storage: 'text', interpretation: { kind: 'json' } }
```

The SDK serializes on write and parses on read - pass and receive the value directly, no `JSON.stringify` / `JSON.parse` on either end.

```ts theme={null}
await create({ title: 'Trip', tags: ['vacation', '2024'] })
// later
record.data.tags  // ['vacation', '2024']
```

## Per-user privacy

Permissions are enforced server-side. To make notes private per user:

```ts theme={null}
permissions: {
  member: { read: 'own', create: true, update: 'own', delete: 'own' },
}
```

`'own'` resolves against `record.createdBy` by default; override with `ownerField` if a different column determines ownership. For published/draft visibility, collaborators, and team rules, see [Permissions](/concepts/permissions).

## Performance tips

* **Filter at the query.** `where` runs server-side, so unauthorized or unwanted rows never cross the wire.
* **Use `limit` on long lists.** Initial subscription sends every matching record. For thousands of rows, slice by date or category. (Cursor pagination is on the roadmap.)
* **Lift `useQuery` to a parent.** Identical queries deduplicate to one WebSocket subscription, but each mount still re-renders on every store change - fetch once at the top of the page and pass records down via props.
* **Patch, don't replace.** `put` with a partial patch keeps the wire payload small and avoids overwriting concurrent edits.

<Note>
  The SDK lints each schema at boot and prints findings prefixed `[schema-lint]` to the worker console. See [Schema-lint warnings](/sdk-reference/worker/schemas#schema-lint-warnings) for the three shapes and their fixes.
</Note>

## Next steps

* [Permissions](/concepts/permissions) - role-based access control on collections.
* [Server actions](/guides/server-actions) - privileged writes that bypass user RBAC.
* [Records reference](/sdk-reference/client/records) - full hooks API surface.
* [Schema reference](/sdk-reference/worker/schemas) - column types and permission rules.
