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 throughDocumentation Index
Fetch the complete documentation index at: https://docs.deep.space/llms.txt
Use this file to discover all available pages before exploring further.
useQuery and useMutations 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 asRecordData<T>:
.data. When you query a todos collection:
Defining a schema
Schemas live undersrc/schemas/, with the full list exported from src/schemas.ts. Every schema has name, columns, and permissions:
Column types
Every column has astorage 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. |
ColumnInterpretation from deepspace/worker if you want the full type for your own helpers:
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'.
Hooks: useQuery and useMutations
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:
createConfirmed returns the same client-generated recordId as create, but doesn’t resolve until the server has acknowledged the write.
Scopes
ARecordScope 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.
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:
How writes flow
When you calluseMutations.create(...), the SDK runs through six steps:
- Optimistic local apply. The new record is added to the in-memory store; React re-renders.
- WebSocket dispatch. A typed message is sent to the
AppRecordRoomDurable Object. - RBAC check. The DO checks the caller’s role (established from their JWT at connect time) against the collection’s
permissions. - SQLite write. The DO persists the record in its local SQLite database.
- Broadcast. The DO sends a
core.record_changeenvelope (withchangeType: 'create' | 'update' | 'delete') to every connected client whose RBAC allows read access. - Reconcile. Other clients apply the update; the originating client confirms its optimistic state.
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.
- Vector search → declare a custom Vectorize binding and call it from your worker.
- Analytics → declare a custom Analytics Engine binding, or use the auto-provisioned
USAGE_EVENTSdataset withmeterUsage. - External SQL → declare a custom D1 database (with
runMigrations) or a Hyperdrive binding to your own Postgres.
Next steps
- Permissions - role-based access control on collections.
- Real-time sync - the WebSocket protocol and consistency guarantees.
- Data storage guide - walkthrough for defining a schema and wiring up CRUD.
- Records reference - full API for
useQueryanduseMutations.