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.
Schemas describe collections - their columns, permissions, and visibility rules. They’re declared in src/schemas/, registered in src/schemas.ts, and baked into the worker at deploy time.
import type {
CollectionSchema,
ColumnDefinition,
ColumnInterpretation,
RolePermissions,
PermissionLevel,
} from 'deepspace/worker'
import {
USERS_COLUMNS, BASE_USERS_SCHEMA,
CHANNELS_SCHEMA, MESSAGES_SCHEMA, REACTIONS_SCHEMA,
CHANNEL_MEMBERS_SCHEMA, CHANNEL_INVITATIONS_SCHEMA, READ_RECEIPTS_SCHEMA,
CONVERSATION_SCHEMAS,
DIRECTORY_SCHEMAS, VOTING_SCHEMAS,
WORKSPACE_SCHEMAS, workspaceTeamsSchema,
SYSTEM_COLLECTIONS,
GLOBAL_DO_TYPES, GLOBAL_DO_TYPE_NAMES,
getGlobalDOType, getGlobalDOSchemas,
RESERVED_COLLECTION_NAMES,
AI_CHATS_SCHEMA, AI_MESSAGES_SCHEMA,
} from 'deepspace/worker'
// Role constants live on the client entry point (shared module):
import { ROLES, ROLE_CONFIG, type Role } from 'deepspace'
CollectionSchema
interface CollectionSchema {
name: string
/** Column definitions - every collection is stored in a typed SQL table. */
columns: ColumnDefinition[]
/** Composite uniqueness constraint (e.g., ['userId', 'taskId']). */
uniqueOn?: string[]
/** Column name used for ownership checks (default: `_created_by`). */
ownerField?: string
/** Column containing JSON array of collaborator user IDs. */
collaboratorsField?: string
/** Column containing team ID for team-based access. */
teamField?: string
/**
* Column controlling per-record read visibility.
* String form: visible when `data[field] === 'public'`.
* Object form: visible when `data[field] === value`.
*/
visibilityField?: string | { field: string; value: unknown }
/** Permissions per role. Use `'*'` for a catch-all fallback. */
permissions: Record<string, RolePermissions>
/** Default role for new users (only on the `users` collection). */
defaultRole?: string
}
ColumnDefinition
interface ColumnDefinition {
/** Stable ID override (survives renames). Falls back to `col_{name}`. */
id?: string
name: string
storage: 'number' | 'text'
interpretation: ColumnInterpretation | string
/** SQL expression for a computed column (read-only). */
expression?: string
/** Auto-populate with the current user ID on create. */
userBound?: boolean
/** Cannot be changed after initial creation. */
immutable?: boolean
/** Must be provided on create (non-null). */
required?: boolean
/** Default value if not provided on create. */
default?: unknown
/** Auto-set ISO timestamp when the named field changes (optionally to a specific value). */
timestampTrigger?: { field: string; value?: unknown }
}
storage is 'number' | 'text' - these are the only two backing SQLite types the SDK uses. interpretation can be either a string shortcut (e.g. 'plain') or a discriminated-union object.
ColumnInterpretation
type ColumnInterpretation =
| { kind: 'plain' }
| { kind: 'currency'; symbol: string; decimals: number }
| { kind: 'date'; format?: string }
| { kind: 'datetime'; format?: string }
| { kind: 'boolean'; trueLabel?: string; falseLabel?: string }
| { kind: 'percent'; decimals?: number }
| { kind: 'select'; options: string[] }
| { kind: 'multiselect'; options: string[] }
| { kind: 'url' }
| { kind: 'email' }
| { kind: 'json' }
| { kind: 'reference'; targetTable: string; displayColumn: string }
Some kinds carry required fields - currency needs symbol and decimals; select and multiselect need options; reference needs targetTable and displayColumn. The bare string form on ColumnDefinition.interpretation is a shortcut for { kind: <string> }.
RolePermissions
interface RolePermissions {
read: PermissionLevel
create: boolean
update: PermissionLevel
delete: PermissionLevel
/** If set, only these columns can be updated by this role. */
writableFields?: string[]
}
type PermissionLevel =
| boolean
| 'own'
| 'unclaimed-or-own'
| 'collaborator'
| 'team'
| 'access'
| 'published'
| 'shared'
All four of read, create, update, delete are required on every role entry. create is boolean-only (you either can or can’t create new rows for a role); the others accept the full PermissionLevel union.
See Concepts → Permissions for the semantics of each level.
Roles
// Imported from `deepspace` (client entry), NOT `deepspace/worker`:
import { ROLES, ROLE_CONFIG, type Role } from 'deepspace'
const ROLES: { VIEWER: 'viewer'; MEMBER: 'member'; ADMIN: 'admin' }
type Role = 'viewer' | 'member' | 'admin'
const ROLE_CONFIG: Record<Role, {
title: string
badgeVariant: 'secondary' | 'default' | 'warning'
description: string
}>
ROLES gives you the three string identifiers used in permissions blocks. ROLE_CONFIG is display metadata for role-badge UIs.
For unauthenticated users, use the '*' key in permissions - there is no anonymous role identifier.
Drop-in schemas - Users
const USERS_COLUMNS: ColumnDefinition[] // canonical users columns
const BASE_USERS_SCHEMA: CollectionSchema // assembled from USERS_COLUMNS
The scaffold’s usersSchema extends BASE_USERS_SCHEMA. Don’t replace; extend if you need extra columns.
Drop-in schemas - Messaging
| Schema | Collection name | Purpose |
|---|
CHANNELS_SCHEMA | channels | Channel definitions (public, private, dm) |
MESSAGES_SCHEMA | messages | Channel messages |
REACTIONS_SCHEMA | reactions | Emoji reactions on messages |
CHANNEL_MEMBERS_SCHEMA | channel_members | Explicit channel membership |
CHANNEL_INVITATIONS_SCHEMA | channel_invitations | Pending invites |
READ_RECEIPTS_SCHEMA | read_receipts | Per-user per-channel read state |
Add to your app’s schemas array to enable the corresponding hooks (useChannels, useMessages, etc.).
Drop-in schemas - Conversations (conv:<id> DOs)
const CONVERSATION_SCHEMAS: CollectionSchema[]
Array of the conv_messages / conv_reactions / conv_members / conv_read_cursors collections. Pass as schemas to a conversation-scope RecordRoom to enable useConversation.
Drop-in schemas - Directory (cross-app)
const DIRECTORY_SCHEMAS: CollectionSchema[] // conversations, conversation_state, communities, memberships, posts
const VOTING_SCHEMAS: CollectionSchema[] // poll-related collections
Hosted on the platform’s directory DO. Rarely instantiated by an app directly - apps mount them via sharedScopes.
Drop-in schemas - Workspace (cross-app shared)
const WORKSPACE_SCHEMAS: CollectionSchema[] // email handles, teams, etc.
const workspaceTeamsSchema: CollectionSchema // teams collection on its own
Mount via sharedScopes for workspace:default.
Drop-in schemas - AI chat
const AI_CHATS_SCHEMA: CollectionSchema // 'ai-chats'
const AI_MESSAGES_SCHEMA: CollectionSchema // 'ai-messages'
RBAC: members read/update/delete: 'own', create: false - writes only flow through the worker’s chat routes. Don’t relax create to true (see the AI chat guide for why).
Global DO type registry
const GLOBAL_DO_TYPES: GlobalDOType[]
const GLOBAL_DO_TYPE_NAMES: readonly string[] // ['workspace', 'conv', 'dir']
function getGlobalDOType(name: string): GlobalDOType | null
function getGlobalDOSchemas(typeName: string): CollectionSchema[]
Runtime lookups for what schemas a given global DO scope expects. getGlobalDOType returns null (not undefined) when the name isn’t registered.
Reserved names
const RESERVED_COLLECTION_NAMES: Set<string> // names you cannot use in app-defined schemas
const SYSTEM_COLLECTIONS: Set<string> // SDK-internal collections (Yjs state, metadata)
Naming a custom collection that collides with either set is rejected at validation time.
Pattern: a typical schema
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: 'tags', storage: 'text', interpretation: { kind: 'json' } },
],
visibilityField: { field: 'status', value: 'published' },
permissions: {
'*': { 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 },
},
}
Schema-lint warnings
When each schema is registered (worker startup, first DO boot), the SDK runs a lightweight lint and prints any findings to the worker console prefixed [schema-lint]. Warnings do not block boot - each just flags a declaration that looks like it should enforce something but doesn’t. Fix every one before shipping.
lintSchema(schema) is also re-exported from deepspace/worker and returns the warning strings as an array, so you can assert against it in your own tests.
import { lintSchema } from 'deepspace/worker'
import { notesSchema } from './schemas/notes-schema'
// In a unit test:
expect(lintSchema(notesSchema)).toEqual([])
1. visibilityField declared but no role uses 'published' / 'shared'
[<collection>] visibilityField is declared but no role uses read: 'published' or 'shared'. Roles with read: true (<roles>) will see every row regardless of visibility. Change those to read: 'published' (owner OR public) or 'shared' (owner OR collaborator OR public) to actually enforce the filter, or remove visibilityField if you don't intend to gate reads by it.
Cause. You set visibilityField (intending per-record gating), but every role with read access has read: true. true is unconditional - the visibility column is never consulted, so every row is visible to every reader.
Fix. Either drop visibilityField, or change at least one role’s read to 'published' (owner OR matches visibilityField) or 'shared' (owner OR collaborator OR matches visibilityField).
This is a privacy foot-gun, not a typo. A schema that triggers this warning will silently leak draft / private rows to everyone who can read the collection.
2. ownerField set but the column is not userBound
[<collection>] ownerField is '<field>' but that column is not marked userBound: true. A client can create a row with someone else's id in this field, bypassing 'own' permission checks. Add userBound: true (and ideally immutable: true) to the column.
Cause. ownerField tells 'own' permission checks which column to read. Without userBound: true on that column, a client can write any user id into it on create - claiming ownership of a row they didn’t actually create.
Fix. Add userBound: true (and ideally immutable: true) to the named column. userBound makes the DO overwrite the field with the caller’s verified user id on every write.
{
name: 'todos',
columns: [
{ name: 'assignedTo', storage: 'text', interpretation: 'plain', userBound: true, immutable: true },
// ...
],
ownerField: 'assignedTo',
permissions: {
member: { read: true, create: true, update: 'own', delete: 'own' },
},
}
3. userBound: true on a non-text column
[<collection>] column '<name>' is userBound but storage is '<storage>'. userBound stamps the user id (a string); use storage: 'text'.
Cause. userBound stamps a user id (a string) into the column. The SDK only coerces strings into 'text' storage; 'number' will fail at write time.
Fix. Change the column’s storage to 'text'.
See also