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

> Collections, records, the envelope shape, and how data flows from your worker to the client.

DeepSpace stores app data in **collections** - typed tables backed by SQLite inside a Durable Object. Each collection is declared in a schema, baked into your worker at deploy time, and exposed to the client through [`useQuery`](/sdk-reference/client/records#usequery\<t>-collection-options) and [`useMutations`](/sdk-reference/client/records#usemutations\<t>-collection) hooks.

## Collections and records

A collection is a named table with typed columns. A record is one row, wrapped in an envelope that carries metadata. The SDK exports this shape as `RecordData<T>`:

```ts theme={null}
type RecordData<T> = {
  recordId: string         // unique ID - the client generates this on create
  data: T                  // your user-defined fields
  createdBy: string        // userId of the creator
  createdAt: string        // ISO timestamp
  updatedAt: string        // ISO timestamp
}
```

Your own fields live under `.data`. When you query a `todos` collection:

```tsx theme={null}
const { records } = useQuery<{ title: string; completed: boolean }>('todos')
records[0].data.title       // "Buy milk"
records[0].recordId         // "1714000000000-k3f9x2a"
records[0].title            // undefined - common bug
```

<Warning>
  Access fields under `r.data.<field>`, not `r.<field>`. Use `r.recordId` to pass into `put` and `remove`.
</Warning>

## Defining a schema

Schemas live under `src/schemas/`, with the full list exported from `src/schemas.ts`. Every schema has `name`, `columns`, and `permissions`:

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

export const itemsSchema: CollectionSchema = {
  name: 'items',
  columns: [
    { name: 'title', storage: 'text', interpretation: 'plain' },
    { name: 'status', storage: 'text', interpretation: { kind: 'select', options: ['draft', 'published'] } },
    { name: 'priority', storage: 'number', interpretation: 'plain' },
  ],
  visibilityField: { field: 'status', value: 'published' },
  permissions: {
    viewer: { read: 'published', create: false, update: false, delete: false },
    member: { read: true, create: true, update: 'own', delete: 'own' },
    admin:  { read: true, create: true, update: true, delete: true },
  },
}
```

Register it:

```ts theme={null}
// src/schemas.ts
export const schemas = [usersSchema, settingsSchema, itemsSchema]
```

Schemas are **baked in at deploy time** - there is no runtime schema registry. Adding or changing a schema requires a redeploy.

## Column types

Every column has a `storage` type and an `interpretation`:

| `storage`  | What it holds                                                                                     |
| ---------- | ------------------------------------------------------------------------------------------------- |
| `'text'`   | Strings, IDs, ISO timestamps, JSON blobs                                                          |
| `'number'` | Integers, floats, booleans (stored as `0`/`1`), and date/datetime values (stored as Unix seconds) |

`storage` picks the underlying SQLite column type - `'text'` becomes a `TEXT` column, `'number'` becomes a `REAL` column. Pick `'number'` for any column you want to range-query, sort numerically, or store as an integer, float, boolean, or Unix timestamp; pick `'text'` for everything else.

`interpretation` tells the SDK how to encode/decode the value. It is either the bare string `'plain'` or an object with a `kind` discriminator:

| Interpretation                                      | Typical `storage`      | Notes                                                                                                                                      |
| --------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `'plain'`                                           | either                 | Pass-through. Use this for raw numbers and free-form text.                                                                                 |
| `{ kind: 'currency', symbol, decimals }`            | `'number'`             | Strips currency symbols / commas on write.                                                                                                 |
| `{ kind: 'date', format? }`                         | `'text'` or `'number'` | ISO date string in text; Unix seconds in number.                                                                                           |
| `{ kind: 'datetime', format? }`                     | `'text'` or `'number'` | Same coercion as `date`.                                                                                                                   |
| `{ kind: 'boolean', trueLabel?, falseLabel? }`      | `'number'`             | Stored as `0` / `1`.                                                                                                                       |
| `{ kind: 'percent', decimals? }`                    | `'number'`             | Accepts `"42%"` strings; stores `0.42`.                                                                                                    |
| `{ kind: 'select', options: string[] }`             | `'text'`               | Constrained enum.                                                                                                                          |
| `{ kind: 'multiselect', options: string[] }`        | `'text'`               | Constrained enum, multiple values. Stored as text - pass a pre-joined string (or use `{ kind: 'json' }` if you want array round-tripping). |
| `{ kind: 'url' }`                                   | `'text'`               | URL string.                                                                                                                                |
| `{ kind: 'email' }`                                 | `'text'`               | Email string.                                                                                                                              |
| `{ kind: 'json' }`                                  | `'text'`               | Auto `JSON.stringify` on write, auto `JSON.parse` on read.                                                                                 |
| `{ kind: 'reference', targetTable, displayColumn }` | `'text'`               | Foreign-key-style pointer to another collection.                                                                                           |

Import the union as `ColumnInterpretation` from `deepspace/worker` if you want the full type for your own helpers:

```ts theme={null}
import type { ColumnInterpretation } from 'deepspace/worker'
```

Use the object form for any kind that takes options (`currency`, `select`, `multiselect`, `reference`). The bare-string form is recommended only for `'plain'` - kinds without required fields technically resolve too, but the object form keeps the schema readable. There is no `'number'` interpretation - express numeric columns as `storage: 'number'` with `interpretation: 'plain'`.

```ts theme={null}
{ name: 'tags', storage: 'text', interpretation: { kind: 'json' } }
// On write: pass the array directly - mutations.create({ tags: ['a', 'b'] })
// On read:  record.data.tags is already an array - don't JSON.parse
```

## Hooks: `useQuery` and `useMutations`

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

type Item = { title: string; status: 'draft' | 'published' }

function ItemList() {
  const { records, status } = useQuery<Item>('items', {
    where: { status: 'published' },
    orderBy: 'createdAt',
    orderDir: 'desc',
    limit: 50,
  })

  const { create, put, remove } = useMutations<Item>('items')

  // create(data: Item) → Promise<string>  (the new recordId)
  // put(id, patch: Partial<Item>) → Promise<void>  (merge into existing row)
  // remove(id) → Promise<void>
}
```

The hooks subscribe to a WebSocket the moment they mount and stream updates in real time. When any user (including you) creates, updates, or deletes a record, every open client sees the change within milliseconds.

### Optimistic vs confirmed mutations

`create` / `put` / `remove` apply changes locally first, then sync to the server. They resolve as soon as the local store is updated - usually before the server confirms.

For workflows that must wait for the server to accept the write - so RBAC denials or schema validation errors surface before you navigate or trigger downstream work - use the `*Confirmed` variants:

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

const { createConfirmed } = useMutations<Item>('items')
try {
  const recordId = await createConfirmed({ title: 'New', status: 'draft' })
  navigate(`/items/${recordId}`)
} catch (err) {
  // server rejected the write - show an error to the user
}
```

`createConfirmed` returns the same client-generated `recordId` as `create`, but doesn't resolve until the server has acknowledged the write.

## Scopes

A `RecordScope` is a single Durable Object that holds all the collections and records mounted inside it. The `roomId` is the DO's identifier - picking a different `roomId` gives you a separate DO with isolated data.

```tsx theme={null}
import { RecordProvider, RecordScope } from 'deepspace'
import { APP_NAME, SCOPE_ID } from './constants'
import { schemas } from './schemas'

<RecordProvider>
  <RecordScope roomId={SCOPE_ID} schemas={schemas} appId={APP_NAME}>
    <App />
  </RecordScope>
</RecordProvider>
```

`SCOPE_ID` from `src/constants.ts` defaults to `app:${APP_NAME}` - your app's main RecordRoom. Each scope is an independent DO with its own data.

Nesting `<RecordScope>` lets you mount additional rooms - for example, a per-conversation DO:

```tsx theme={null}
<RecordScope roomId={`conv:${convId}`} schemas={CONVERSATION_SCHEMAS} appId={APP_NAME}>
  <ChatThread />
</RecordScope>
```

## How writes flow

When you call `useMutations.create(...)`, the SDK runs through six steps:

1. **Optimistic local apply.** The new record is added to the in-memory store; React re-renders.
2. **WebSocket dispatch.** A typed message is sent to the `AppRecordRoom` Durable Object.
3. **RBAC check.** The DO checks the caller's role (established from their JWT at connect time) against the collection's `permissions`.
4. **SQLite write.** The DO persists the record in its local SQLite database.
5. **Broadcast.** The DO sends a `core.record_change` envelope (with `changeType: 'create' | 'update' | 'delete'`) to every connected client whose RBAC allows read access.
6. **Reconcile.** Other clients apply the update; the originating client confirms its optimistic state.

On the wire there is a single record-change message: `core.record_change`, carrying a `changeType` discriminator. The client store fans this out into internal `record_created` / `record_updated` / `record_removed` notifications for the React subscriptions - those names are SDK-internal and not part of the wire vocabulary.

If the server rejects the write - an RBAC denial or a schema validation failure - the optimistic update rolls back automatically.

## Beyond records

DeepSpace records are tuned for **operational data** - collections of hundreds to tens of thousands of small rows, queried by client filters and updated frequently. For larger or analytical workloads:

* **Files / blobs** → use [R2 file storage](/guides/file-uploads).
* **Vector search** → declare a [custom Vectorize binding](/guides/custom-bindings) and call it from your worker.
* **Analytics** → declare a custom Analytics Engine binding, or use the auto-provisioned `USAGE_EVENTS` dataset with [`meterUsage`](/sdk-reference/worker/bindings).
* **External SQL** → declare a custom D1 database (with `runMigrations`) or a Hyperdrive binding to your own Postgres.

## Next steps

* [Permissions](/concepts/permissions) - role-based access control on collections.
* [Real-time sync](/concepts/realtime-sync) - the WebSocket protocol and consistency guarantees.
* [Data storage guide](/guides/data-storage) - walkthrough for defining a schema and wiring up CRUD.
* [Records reference](/sdk-reference/client/records) - full API for `useQuery` and `useMutations`.
