Skip to main content

typescript-server

SpacetimeDB TypeScript server module SDK reference. Use when writing tables, reducers, or module logic in TypeScript.

Source facts

Repository
clockworklabs/SpacetimeDB
Last source activity
November 4, 2026 at 18:46
Detected SKILL.md language
English
Stars
25,531
Forks
1,117

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
typescript-server
description
SpacetimeDB TypeScript server module SDK reference. Use when writing tables, reducers, or module logic in TypeScript.
license
Apache-2.0
metadata
{"author":"clockworklabs","version":"2.0","role":"server","language":"typescript","cursor_globs":"**/*.ts","cursor_always_apply":true}
# SpacetimeDB TypeScript SDK Reference ## Module Structure Tables are built with `table()`, bound with `schema()`, and exported as default. Reducers and lifecycle hooks are `export const`: ```typescript import { schema, table, t } from 'spacetimedb/server'; const scoreRecord = table( { name: 'score_record', public: true }, { id: t.u64().primaryKey().autoInc(), owner: t.identity(), value: t.u32(), } ); const spacetimedb = schema({ scoreRecord }); // ONE object, not spread args export default spacetimedb; export const addRecord = spacetimedb.reducer( { value: t.u32() }, (ctx, { value }) => { ctx.db.scoreRecord.insert({ id: 0n, owner: ctx.sender, value }); } ); ``` `schema({...})` accepts table definitions and submodule namespace registrations. Row and object builders used as reducer arguments or view return types are not schema entries. Environment declarations go in the second argument: `schema(tables, { env: { ... } })`. Export the default schema and every reducer, lifecycle hook, view, procedure, HTTP export, and visibility filter to register. Root-entry named exports must be registered SpacetimeDB values: keep ordinary root helpers/constants unexported. A submodule library may export ordinary helpers for its consumer to call, because submodule discovery ignores non-module exports; do not re-export those helpers from the root entry. ## Imports Schema builders and module exports come from `spacetimedb/server`. Runtime value classes such as `ScheduleAt`, `Timestamp`, and `ConnectionId` come from the root `spacetimedb` package; `Range` comes from `spacetimedb/server`: ```typescript import { schema, table, t, SenderError, type InferSchema, type ReducerCtx, } from 'spacetimedb/server'; import { ConnectionId, ScheduleAt, Timestamp } from 'spacetimedb'; import { Range } from 'spacetimedb/server'; ``` ## Tables `table(OPTIONS, COLUMNS)` takes two arguments. The `name` field MUST be snake_case: ```typescript const entity = table( { name: 'entity', public: true }, { identity: t.identity().primaryKey(), name: t.string(), active: t.bool(), } ); ``` Options: `name` (snake_case, recommended), `public: true`, `event: true`, `indexes: [...]` `ctx.db` accessors are the keys passed to `schema({...})`, verbatim: `schema({ scoreRecord })` -> `ctx.db.scoreRecord`. Keep TypeScript identifiers and accessors camelCase. Use explicit `name: 'snake_case'` strings when you need a canonical database name that differs from the TypeScript identifier. ## Column Types Every column is a `t` builder value: | Builder | JS type | Notes | |---------|---------|-------| | `t.u8()` / `t.u16()` / `t.u32()` | number | | | `t.i8()` / `t.i16()` / `t.i32()` | number | | | `t.u64()` | bigint | Use `0n` literals | | `t.i64()` | bigint | Use `0n` literals | | `t.u128()` / `t.i128()` / `t.u256()` / `t.i256()` | bigint | | | `t.f64()` / `t.f32()` | number | | | `t.bool()` | boolean | | | `t.string()` | string | | | `t.identity()` | Identity | | | `t.connectionId()` | ConnectionId | | | `t.timestamp()` | Timestamp | | | `t.timeDuration()` | TimeDuration | | | `t.scheduleAt()` | ScheduleAt | | Modifiers: `.primaryKey()`, `.autoInc()`, `.unique()`, `.index('btree')`, `.default(value)`. Use `.default(value)` only for a newly appended migration-safe field. Do not put defaults on primary-key, unique, or auto-increment columns. Optional columns: `nickname: t.option(t.string())` Schema builders describe the database's wire types; they are not TypeScript type names. For example, a `t.u16()` value is a TypeScript `number`, not a value cast to a type named `u16`. ## Indexes Use inline `.index('btree')` when a single-column index does not need a named accessor. Use an `indexes` entry when the accessor is named explicitly or the index spans multiple columns. Every `indexes` entry requires `columns`; do not also add `.index('btree')` to the same column. ```typescript // Inline (preferred for single-column): authorId: t.u64().index('btree'), // Access: ctx.db.post.authorId.filter(authorId); // Multi-column (named): indexes: [{ accessor: 'byGroupUser', algorithm: 'btree', columns: ['groupId', 'userId'] }] // Access: ctx.db.membership.byGroupUser.filter([groupId, userId]); ``` Prefer a multi-column index over filtering by one column and looping. Filter takes an array in index column order; a prefix scan passes the leading value bare: `filter(groupId)`. The published module's **entry file must export the schema as default**. If you split tables (`schema.ts`) from reducers/lifecycle (`index.ts`), re-export it from the entry: ```typescript // index.ts export { default } from './schema'; // re-export the schema for the module entry ``` ## Reducers Reducers are created with `spacetimedb.reducer(...)`; the export name becomes the reducer name: ```typescript export const createEntity = spacetimedb.reducer( { name: t.string(), age: t.i32() }, (ctx, { name, age }) => { ctx.db.entity.insert({ identity: ctx.sender, name, age, active: true }); } ); // No arguments, just the callback: export const doReset = spacetimedb.reducer((ctx) => { ... }); ``` Reducer args accept any column type, including arrays of custom types: `{ splits: t.array(Split) }`. Do not pass JSON strings for structured data. ## DB Operations ```typescript ctx.db.scoreRecord.insert({ id: 0n, owner: ctx.sender, value: 1 }); // Insert (0n for autoInc) ctx.db.scoreRecord.id.find(recordId); // Find by PK → row | null ctx.db.entity.identity.find(ctx.sender); // Find by unique column [...ctx.db.post.authorId.filter(authorId)]; // Filter → spread to Array [...ctx.db.entity.iter()]; // All rows → Array ctx.db.scoreRecord.id.update({ ...existing, value: 2 }); // Update (spread + override) ctx.db.scoreRecord.id.delete(recordId); // Delete by PK ``` Insert through the table accessor (`ctx.db.scoreRecord.insert(...)`). Primary-key, unique, and index accessors support lookup or mutation of existing rows, but do not have `insert(...)`. `insert(...)` returns the inserted row, including database-assigned auto-increment fields. The accessor for a primary key or index is the declared column name. For example, a primary key named `eventId` is accessed as `ctx.db.event.eventId`, not `ctx.db.event.id`. The schema value registers module exports but does not expose database rows. Pass a context into any helper that needs `ctx.db`. Note: `iter()` and `filter()` return iterators. Spread to Array for `.sort()`, `.filter()`, `.map()`. ## Lifecycle Hooks MUST be `export const`. Bare calls are silently ignored: ```typescript export const init = spacetimedb.init((ctx) => { ... }); export const onConnect = spacetimedb.clientConnected((ctx) => { ... }); export const onDisconnect = spacetimedb.clientDisconnected((ctx) => { ... }); ``` `ctx.connectionId` is `ConnectionId | null`, including in lifecycle contexts. Guard it before passing it to a helper or using it as a table key. ## Reducer Context API `ctx` is the only source of sender identity, time, and randomness; stdlib clocks and RNG are unavailable in modules. Let exported callbacks infer their context type. In helpers, use `ReducerCtx<InferSchema<typeof spacetimedb>>`; do not annotate a context as `any`, because that erases table row types and can make `bigint` expressions infer as `number`. ```typescript type Ctx = ReducerCtx<InferSchema<typeof spacetimedb>>; function findRecord(ctx: Ctx, id: bigint) { return ctx.db.scoreRecord.id.find(id); } ``` ```typescript // Auth: ctx.sender is the caller's Identity if (!row.owner.equals(ctx.sender)) throw new SenderError('unauthorized'); // ctx.connectionId: the per-connection id, NULLABLE (ConnectionId | null) — null-check before use. // One Identity can hold several connections (multiple tabs/devices). if (ctx.connectionId) { /* ... */ } // Server timestamp (deterministic per reducer call) ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp }); // Deterministic RNG const f: number = ctx.random(); // [0.0, 1.0) const roll: number = ctx.random.integerInRange(1, 6); // safe JS number bounds/result, inclusive const storedRoll: bigint = BigInt(roll); // convert the result for an i64/u64 column const bytes: Uint8Array = ctx.random.fill(new Uint8Array(16)); // 2.13+: in-place shuffle; copy first to preserve the original array. const shuffled = ctx.random.shuffle([...cards]); // Client: Timestamp → Date new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n)); ``` Do not construct `Identity` values from strings (e.g. `'hex' as Identity`): serialization fails and kills the module. Identities come from `ctx.sender` or `t.identity()` columns. Construct a `ConnectionId` from its numeric representation with `new ConnectionId(value)` after importing `ConnectionId` from `spacetimedb`. Construct exact timestamps with `new Timestamp(micros)` after importing `Timestamp` from `spacetimedb`. Inclusive index ranges use `Range` from `spacetimedb/server`: ```typescript import { Timestamp } from 'spacetimedb'; import { Range } from 'spacetimedb/server'; ctx.db.shipment.deliverBy.filter(new Range( { tag: 'included', value: new Timestamp(1_000n) }, { tag: 'included', value: new Timestamp(2_000n) }, )); ``` ## Scheduled Tables The reducer or procedure referenced by a table's `scheduled` option must be exported. ```typescript import { ScheduleAt } from 'spacetimedb'; // ScheduleAt comes from the root package const tickTimer = table({ name: 'tick_timer', }, { scheduledId: t.u64().primaryKey().autoInc(), scheduledAt: t.scheduleAt(), }); export const tick = spacetimedb.reducer( { onSchedule: tickTimer }, { timer: tickTimer.rowType }, (ctx, { timer }) => { /* timer row auto-deleted after this runs */ } ); // `onSchedule` also works for scheduled procedures whose return type is `t.unit()`. // Legacy table-side scheduling, `scheduled: (): any => tick`, still works but is not // recommended for new code because it forces a forward reference. // One-time: ScheduleAt.time(ctx.timestamp.microsSinceUnixEpoch + delayMicros) // Repeating: ScheduleAt.interval(60_000_000n) // Read time back from a scheduleAt value (tagged union): // const micros = at.tag === 'time' ? at.value : at.value.microsSinceUnixEpoch; // bigint ``` ## Submodules Import the library's full module-namespace object, not just its default schema: ```typescript import * as authLib from 'auth_lib'; const spacetimedb = schema({ scoreRecord, myAuth: { name: 'auth_data', module: authLib }, }); export default spacetimedb; ``` `myAuth` is the accessor (`ctx.db.myAuth.<table>`); `auth_data` is the canonical namespace used in SQL and wire names. Without an explicit `name`, the root's case policy derives it (`myAuth` → `my_auth` by default). Pin `name` when renaming an accessor: changing the canonical namespace drops its tables and their data. The library keeps its own flat `ctx.db` accessors. To call a library function directly, pass `ctx.as.myAuth`; inside `withTx`, use `tx.as.myAuth`. Nested TypeScript submodules are supported. Lifecycle reducers and nonempty environment declarations belong in the root. Submodule routers are ignored; register delegated handlers on the root router. Composition is not cross-language. Official reusable libraries include `@spacetimedb/cron`, `@spacetimedb/retry`, `@spacetimedb/rate-limit`, and `@spacetimedb/crypto`. Their registration shapes differ: `@spacetimedb/rate-limit/submodule` is mountable, cron/retry expose factories, and crypto exports helpers. Consult the chosen package's README/exports rather than assuming every package root is a mountable module. ## Environment Variables ```typescript const spacetimedb = schema({ scoreRecord }, { env: { API_KEY: t.string(), MODE: t.enum('Mode', ['development', 'production']), LOG_LEVEL: t.string().optional(), }, }); // In a root reducer/procedure/view/handler: // ctx.env.API_KEY: string; ctx.env.LOG_LEVEL: string | undefined // ctx.env.get('LOG_LEVEL'): string | null ```
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub