| 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
Imports
import { schema, table, t } from 'spacetimedb/server';
import { SenderError } from 'spacetimedb/server';
import { ScheduleAt } from 'spacetimedb';
Tables
table(OPTIONS, COLUMNS) takes two arguments. The name field MUST be snake_case:
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, scheduled: (): any => reducerRef, indexes: [...]
ctx.db accessors are the camelCase form of the table's name field.
Column Types
| Builder | JS type | Notes |
|---|
t.u64() | bigint | Use 0n literals |
t.i64() | bigint | Use 0n literals |
t.u32() / t.i32() | number | |
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')
Optional columns: nickname: t.option(t.string())
Indexes
Prefer inline .index('btree') for single-column. Use named indexes only for multi-column:
authorId: t.u64().index('btree'),
indexes: [{ accessor: 'by_group_user', algorithm: 'btree', columns: ['groupId', 'userId'] }]
When you frequently look up rows by multiple columns, prefer a multi-column index over filtering by one column and looping over the results. Multi-column filter takes an array matching the index column order. You can omit trailing columns to do a prefix scan.
Schema Export
const spacetimedb = schema({ entity, record });
export default spacetimedb;
Reducers
Export name becomes the reducer name:
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 });
}
);
export const doReset = spacetimedb.reducer((ctx) => { ... });
DB Operations
ctx.db.entity.insert({ id: 0n, name: 'Sample' });
ctx.db.entity.id.find(entityId);
ctx.db.entity.identity.find(ctx.sender);
[...ctx.db.item.authorId.filter(authorId)];
[...ctx.db.entity.iter()];
ctx.db.entity.id.update({ ...existing, name: newName });
ctx.db.entity.id.delete(entityId);
Note: iter() and filter() return iterators. Spread to Array for .sort(), .filter(), .map().
Lifecycle Hooks
MUST be export const. Bare calls are silently ignored:
export const init = spacetimedb.init((ctx) => { ... });
export const onConnect = spacetimedb.clientConnected((ctx) => { ... });
export const onDisconnect = spacetimedb.clientDisconnected((ctx) => { ... });
Reducer Context API
ReducerContext is the single source of sender identity, deterministic time, and deterministic randomness inside a reducer. Always go through ctx for these. Standard library clocks and random sources are not available in modules.
if (!row.owner.equals(ctx.sender)) throw new SenderError('unauthorized');
ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp });
const f: number = ctx.random();
const roll: number = ctx.random.integerInRange(1, 6);
const bytes: Uint8Array = ctx.random.fill(new Uint8Array(16));
new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));
Scheduled Tables
const tickTimer = table({
name: 'tick_timer',
scheduled: (): any => tick,
}, {
scheduled_id: t.u64().primaryKey().autoInc(),
scheduled_at: t.scheduleAt(),
});
export const tick = spacetimedb.reducer(
{ timer: tickTimer.rowType },
(ctx, { timer }) => { }
);
Custom Types
const Position = t.object('Position', { x: t.i32(), y: t.i32() });
const entity = table({ name: 'entity' }, {
id: t.u64().primaryKey().autoInc(),
pos: Position,
});
const Shape = t.enum('Shape', {
circle: t.i32(),
rectangle: t.object('Rect', { w: t.i32(), h: t.i32() }),
});
Views
export const activeUsers = spacetimedb.anonymousView(
{ name: 'active_users', public: true },
t.array(entity.rowType),
(ctx) => [...ctx.db.entity.iter()].filter(e => e.active)
);
export const myProfile = spacetimedb.view(
{ name: 'my_profile', public: true },
t.option(entity.rowType),
(ctx) => ctx.db.entity.identity.find(ctx.sender) ?? undefined
);
Complete Example
import { schema, table, t } from 'spacetimedb/server';
const entity = table(
{ name: 'entity', public: true },
{
identity: t.identity().primaryKey(),
name: t.string(),
active: t.bool(),
}
);
const record = table(
{
name: 'record',
public: true,
indexes: [{ accessor: 'by_owner', algorithm: 'btree', columns: ['owner'] }],
},
{
id: t.u64().primaryKey().autoInc(),
owner: t.identity(),
value: t.u32(),
}
);
const spacetimedb = schema({ entity, record });
export default spacetimedb;
export const onConnect = spacetimedb.clientConnected((ctx) => {
const existing = ctx.db.entity.identity.find(ctx.sender);
if (existing) ctx.db.entity.identity.update({ ...existing, active: true });
});
export const onDisconnect = spacetimedb.clientDisconnected((ctx) => {
const existing = ctx.db.entity.identity.find(ctx.sender);
if (existing) ctx.db.entity.identity.update({ ...existing, active: false });
});
export const createEntity = spacetimedb.reducer(
{ name: t.string() },
(ctx, { name }) => {
if (ctx.db.entity.identity.find(ctx.sender)) throw new Error('already exists');
ctx.db.entity.insert({ identity: ctx.sender, name, active: true });
}
);
export const addRecord = spacetimedb.reducer(
{ value: t.u32() },
(ctx, { value }) => {
if (!ctx.db.entity.identity.find(ctx.sender)) throw new Error('not found');
ctx.db.record.insert({ id: 0n, owner: ctx.sender, value });
}
);