بنقرة واحدة
typescript-server
// SpacetimeDB TypeScript server module SDK reference. Use when writing tables, reducers, or module logic in TypeScript.
// SpacetimeDB TypeScript server module SDK reference. Use when writing tables, reducers, or module logic in TypeScript.
SpacetimeDB CLI reference for initializing projects, building modules, publishing databases, querying data, and managing servers
Understand SpacetimeDB architecture and core concepts. Use when learning SpacetimeDB or making architectural decisions.
SpacetimeDB C++ server module SDK reference. Use when writing tables, reducers, or module logic in C++.
SpacetimeDB C#/.NET client SDK reference. Use when building C# clients that connect to SpacetimeDB (console, desktop, or any .NET app).
SpacetimeDB C# server module SDK reference. Use when writing tables, reducers, or module logic in C#.
SpacetimeDB Rust server module SDK reference. Use when writing tables, reducers, or module logic in Rust.
| 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} |
import { schema, table, t } from 'spacetimedb/server';
import { SenderError } from 'spacetimedb/server';
import { ScheduleAt } from 'spacetimedb'; // for scheduled tables only
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.
| 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())
Prefer inline .index('btree') for single-column. Use named indexes only for multi-column:
// Inline (preferred for single-column):
authorId: t.u64().index('btree'),
// Access: ctx.db.post.authorId.filter(authorId);
// Multi-column (named):
indexes: [{ accessor: 'by_group_user', algorithm: 'btree', columns: ['groupId', 'userId'] }]
// Access: ctx.db.membership.by_group_user.filter([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.
const spacetimedb = schema({ entity, record }); // ONE object, not spread args
export default spacetimedb;
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 });
}
);
// No arguments, just the callback:
export const doReset = spacetimedb.reducer((ctx) => { ... });
ctx.db.entity.insert({ id: 0n, name: 'Sample' }); // Insert (0n for autoInc)
ctx.db.entity.id.find(entityId); // Find by PK → row | null
ctx.db.entity.identity.find(ctx.sender); // Find by unique column
[...ctx.db.item.authorId.filter(authorId)]; // Filter → spread to Array
[...ctx.db.entity.iter()]; // All rows → Array
ctx.db.entity.id.update({ ...existing, name: newName }); // Update (spread + override)
ctx.db.entity.id.delete(entityId); // Delete by PK
Note: iter() and filter() return iterators. Spread to Array for .sort(), .filter(), .map().
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) => { ... });
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.
// Auth: ctx.sender is the caller's Identity
if (!row.owner.equals(ctx.sender)) throw new SenderError('unauthorized');
// 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); // inclusive
const bytes: Uint8Array = ctx.random.fill(new Uint8Array(16));
// Client: Timestamp → Date
new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));
const tickTimer = table({
name: 'tick_timer',
scheduled: (): any => tick, // (): any => breaks circular dep
}, {
scheduled_id: t.u64().primaryKey().autoInc(),
scheduled_at: t.scheduleAt(),
});
export const tick = spacetimedb.reducer(
{ timer: tickTimer.rowType },
(ctx, { timer }) => { /* timer row auto-deleted after this runs */ }
);
// One-time: ScheduleAt.time(ctx.timestamp.microsSinceUnixEpoch + delayMicros)
// Repeating: ScheduleAt.interval(60_000_000n)
// Product type (struct):
const Position = t.object('Position', { x: t.i32(), y: t.i32() });
const entity = table({ name: 'entity' }, {
id: t.u64().primaryKey().autoInc(),
pos: Position,
});
// Sum type (tagged union):
const Shape = t.enum('Shape', {
circle: t.i32(),
rectangle: t.object('Rect', { w: t.i32(), h: t.i32() }),
});
// Values: { tag: 'circle', value: 10 }
// Anonymous view (same for all clients):
export const activeUsers = spacetimedb.anonymousView(
{ name: 'active_users', public: true },
t.array(entity.rowType),
(ctx) => [...ctx.db.entity.iter()].filter(e => e.active)
);
// Per-user view (varies by ctx.sender):
export const myProfile = spacetimedb.view(
{ name: 'my_profile', public: true },
t.option(entity.rowType),
(ctx) => ctx.db.entity.identity.find(ctx.sender) ?? undefined
);
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 });
}
);