spacetimedb-typescript

Build TypeScript clients for SpacetimeDB. Use when connecting to SpacetimeDB from web apps, Node.js, Deno, Bun, or other JavaScript runtimes.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "spacetimedb-typescript" with this command: npx skills add clockworklabs/spacetimedb/clockworklabs-spacetimedb-spacetimedb-typescript

SpacetimeDB TypeScript SDK

Build real-time TypeScript clients that connect directly to SpacetimeDB modules. The SDK provides type-safe database access, automatic synchronization, and reactive updates for web apps, Node.js, Deno, Bun, and other JavaScript runtimes.


HALLUCINATED APIs — DO NOT USE

These APIs DO NOT EXIST. LLMs frequently hallucinate them.

// WRONG PACKAGE — does not exist
import { SpacetimeDBClient } from "@clockworklabs/spacetimedb-sdk";

// WRONG — these methods don't exist
SpacetimeDBClient.connect(...);
SpacetimeDBClient.call("reducer_name", [...]);
connection.call("reducer_name", [arg1, arg2]);

// WRONG — positional reducer arguments
conn.reducers.doSomething("value");  // WRONG!

// WRONG — old 1.0 patterns
spacetimedb.reducer('reducer_name', params, fn);  // Use export const name = spacetimedb.reducer(params, fn)
schema(myTable);          // Use schema({ myTable })
schema(t1, t2, t3);      // Use schema({ t1, t2, t3 })
scheduled: 'run_cleanup'  // Use scheduled: () => run_cleanup
.withModuleName('db')     // Use .withDatabaseName('db') (2.0)
setReducerFlags.x('NoSuccessNotify')  // Removed in 2.0

CORRECT PATTERNS:

// CORRECT IMPORTS
import { DbConnection, tables } from './module_bindings';  // Generated!
import { SpacetimeDBProvider, useTable } from 'spacetimedb/react';
import { Identity } from 'spacetimedb';

// CORRECT REDUCER CALLS — object syntax, not positional!
conn.reducers.doSomething({ value: 'test' });
conn.reducers.updateItem({ itemId: 1n, newValue: 42 });

// CORRECT DATA ACCESS — useTable returns [rows, isReady]
const [items, isReady] = useTable(tables.item);

DO NOT:

  • Invent hooks like useItems(), useData() — use useTable(tables.tableName)
  • Import from fake packages — only spacetimedb, spacetimedb/react, ./module_bindings

Common Mistakes Table

Server-side errors

WrongRightError
Missing package.jsonCreate package.json"could not detect language"
Missing tsconfig.jsonCreate tsconfig.json"TsconfigNotFound"
Entrypoint not at src/index.tsUse src/index.tsModule won't bundle
indexes in COLUMNS (2nd arg)indexes in OPTIONS (1st arg) of table()"reading 'tag'" error
Index without algorithmalgorithm: 'btree'"reading 'tag'" error
filter({ ownerId })filter(ownerId)"does not exist in type 'Range'"
.filter() on unique column.find() on unique columnTypeError
insert({ ...without id })insert({ id: 0n, ... })"Property 'id' is missing"
const id = table.insert(...)const row = table.insert(...).insert() returns ROW, not ID
.unique() + explicit indexJust use .unique()"name is used for multiple entities"
Import spacetimedb from index.tsImport from schema.ts"Cannot access before initialization"
Incorrect multi-column .filter() range shapeMatch index prefix/tuple shapeEmpty results or range/type errors
.iter() in viewsUse index lookups onlyViews can't scan tables
ctx.db in proceduresctx.withTx(tx => tx.db...)Procedures need explicit transactions

Client-side errors

WrongRightError
Inline connectionBuilderuseMemo(() => ..., [])Reconnects every render
const rows = useTable(table)const [rows, isReady] = useTable(table)Tuple destructuring
Optimistic UI updatesLet subscriptions drive stateDesync issues
<SpacetimeDBProvider builder={...}>connectionBuilder={...}Wrong prop name

Hard Requirements

  1. schema({ table }) — use a single tables object; optional module settings are allowed as a second argument
  2. Reducer/procedure names from exportsexport const name = spacetimedb.reducer(params, fn); never reducer('name', ...)
  3. Reducer calls use object syntax{ param: 'value' } not positional args
  4. Import DbConnection from ./module_bindings — not from spacetimedb
  5. DO NOT edit generated bindings — regenerate with spacetime generate
  6. Indexes go in OPTIONS (1st arg) — not in COLUMNS (2nd arg) of table()
  7. Use BigInt for u64/i64 fields0n, 1n, not 0, 1
  8. Reducers are transactional — they do not return data
  9. Reducers must be deterministic — no filesystem, network, timers, random
  10. Views should use index lookups.iter() causes severe performance issues
  11. Procedures need ctx.withTx()ctx.db doesn't exist in procedures
  12. Sum type values — use { tag: 'variant', value: payload } not { variant: payload }
  13. Use .withDatabaseName() — not .withModuleName() (2.0)

Installation

npm install spacetimedb

For Node.js environments without native fetch/WebSocket support, install undici.

Generating Type Bindings

spacetime generate --lang typescript --out-dir ./src/module_bindings --module-path ./server

Client Connection

import { DbConnection } from './module_bindings';

const connection = DbConnection.builder()
  .withUri('ws://localhost:3000')
  .withDatabaseName('my_database')
  .withToken(localStorage.getItem('spacetimedb_token') ?? undefined)
  .onConnect((conn, identity, token) => {
    // identity: your unique Identity for this database
    console.log('Connected as:', identity.toHexString());

    // Save token for reconnection (preserves identity across sessions)
    localStorage.setItem('spacetimedb_token', token);

    conn.subscriptionBuilder()
      .onApplied(() => console.log('Cache ready'))
      .subscribe('SELECT * FROM player');
  })
  .onDisconnect((ctx) => console.log('Disconnected'))
  .onConnectError((ctx, error) => console.error('Connection failed:', error))
  .build();

Subscribing to Tables

// Basic subscription
connection.subscriptionBuilder()
  .onApplied((ctx) => console.log('Cache ready'))
  .subscribe('SELECT * FROM player');

// Multiple queries
connection.subscriptionBuilder()
  .subscribe(['SELECT * FROM player', 'SELECT * FROM game_state']);

// Subscribe to all tables (development only — cannot mix with Subscribe)
connection.subscriptionBuilder().subscribeToAllTables();

// Subscription handle for later unsubscribe
const handle = connection.subscriptionBuilder()
  .onApplied(() => console.log('Subscribed'))
  .subscribe('SELECT * FROM player');

handle.unsubscribeThen(() => console.log('Unsubscribed'));

Accessing Table Data

for (const player of connection.db.player.iter()) { console.log(player.name); }
const players = Array.from(connection.db.player.iter());
const count = connection.db.player.count();
const player = connection.db.player.id.find(42n);

Table Event Callbacks

connection.db.player.onInsert((ctx, player) => console.log('New:', player.name));
connection.db.player.onDelete((ctx, player) => console.log('Left:', player.name));
connection.db.player.onUpdate((ctx, old, new_) => console.log(`${old.score} -> ${new_.score}`));

Calling Reducers

CRITICAL: Use object syntax, not positional arguments.

connection.reducers.createPlayer({ name: 'Alice', location: { x: 0, y: 0 } });

Snake_case to camelCase conversion

  • Server: export const do_something = spacetimedb.reducer(...)
  • Client: conn.reducers.doSomething({ ... })

Identity and Authentication

  • identity and token are provided in the onConnect callback (see Client Connection above)
  • identity.toHexString() for display or logging
  • Omit .withToken() for anonymous connection — server assigns a new identity
  • Pass a stale/invalid token: server issues a new identity and token in onConnect

Error Handling

Connection-level errors (.onConnectError, .onDisconnect) are shown in the Client Connection example above.

// Subscription error
connection.subscriptionBuilder()
  .onApplied(() => console.log('Subscribed'))
  .onError((ctx) => console.error('Subscription error:', ctx.event))
  .subscribe('SELECT * FROM player');

Server-Side Module Development

Table Definition

import { schema, table, t } from 'spacetimedb/server';

export const Task = table({
  name: 'task',
  public: true,
  indexes: [{ name: 'task_owner_id', algorithm: 'btree', columns: ['ownerId'] }]
}, {
  id: t.u64().primaryKey().autoInc(),
  ownerId: t.identity(),
  title: t.string(),
  createdAt: t.timestamp(),
});

Column types

t.identity()           // User identity
t.u64()                // Unsigned 64-bit integer (use for IDs)
t.string()             // Text
t.bool()               // Boolean
t.timestamp()          // Timestamp
t.scheduleAt()         // For scheduled tables only
t.object('Name', {})   // Product types (nested objects)
t.enum('Name', {})     // Sum types (tagged unions)
t.string().optional()  // Nullable

BigInt syntax: All u64/i64 fields use 0n, 1n, not 0, 1.

Schema export

const spacetimedb = schema({ Task, Player });
export default spacetimedb;

Reducer Definition (2.0)

Name comes from the export — NOT from a string argument.

import spacetimedb from './schema';
import { t, SenderError } from 'spacetimedb/server';

export const create_task = spacetimedb.reducer(
  { title: t.string() },
  (ctx, { title }) => {
    if (!title) throw new SenderError('title required');
    ctx.db.task.insert({ id: 0n, ownerId: ctx.sender, title, createdAt: ctx.timestamp });
  }
);

Update Pattern

const existing = ctx.db.task.id.find(taskId);
if (!existing) throw new SenderError('Task not found');
ctx.db.task.id.update({ ...existing, title: newTitle, updatedAt: ctx.timestamp });

Lifecycle Hooks

spacetimedb.clientConnected((ctx) => { /* ctx.sender is the connecting identity */ });
spacetimedb.clientDisconnected((ctx) => { /* clean up */ });

Event Tables (2.0)

Reducer callbacks are removed in 2.0. Use event tables + onInsert instead.

export const DamageEvent = table(
  { name: 'damage_event', public: true, event: true },
  { target: t.identity(), amount: t.u32() }
);

export const deal_damage = spacetimedb.reducer(
  { target: t.identity(), amount: t.u32() },
  (ctx, { target, amount }) => {
    ctx.db.damageEvent.insert({ target, amount });
  }
);

Client subscribes and uses onInsert:

conn.db.damageEvent.onInsert((ctx, evt) => {
  playDamageAnimation(evt.target, evt.amount);
});

Event tables must be subscribed explicitly — they are excluded from subscribeToAllTables().


Views

ViewContext vs AnonymousViewContext

// ViewContext — has ctx.sender, result varies per user
spacetimedb.view({ name: 'my_items', public: true }, t.array(Item.rowType), (ctx) => {
  return [...ctx.db.item.by_owner.filter(ctx.sender)];
});

// AnonymousViewContext — no ctx.sender, same result for everyone (better perf)
spacetimedb.anonymousView({ name: 'leaderboard', public: true }, t.array(Player.rowType), (ctx) => {
  return ctx.from.player.where(p => p.score.gt(1000));
});

Views can only use index lookups — .iter() is NOT allowed.


Scheduled Tables

export const CleanupJob = table({
  name: 'cleanup_job',
  scheduled: () => run_cleanup  // function returning the exported reducer
}, {
  scheduledId: t.u64().primaryKey().autoInc(),
  scheduledAt: t.scheduleAt(),
  targetId: t.u64(),
});

export const run_cleanup = spacetimedb.reducer(
  { arg: CleanupJob.rowType },
  (ctx, { arg }) => { /* arg.scheduledId, arg.targetId available */ }
);

// Schedule a job
import { ScheduleAt } from 'spacetimedb';
ctx.db.cleanupJob.insert({
  scheduledId: 0n,
  scheduledAt: ScheduleAt.time(ctx.timestamp.microsSinceUnixEpoch + 60_000_000n),
  targetId: someId
});

ScheduleAt on Client

// ScheduleAt is a tagged union on the client
// { tag: 'Time', value: Timestamp } or { tag: 'Interval', value: TimeDuration }
const schedule = row.scheduledAt;
if (schedule.tag === 'Time') {
  const date = new Date(Number(schedule.value.microsSinceUnixEpoch / 1000n));
}

Timestamps

Server-side

ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp });
const future = ctx.timestamp.microsSinceUnixEpoch + 300_000_000n;

Client-side

// Timestamps are objects with BigInt, not numbers
const date = new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));

Procedures (Beta)

export const fetch_data = spacetimedb.procedure(
  { url: t.string() }, t.string(),
  (ctx, { url }) => {
    const response = ctx.http.fetch(url);
    ctx.withTx(tx => { tx.db.myTable.insert({ id: 0n, content: response.text() }); });
    return response.text();
  }
);

Procedures don't have ctx.db — use ctx.withTx(tx => tx.db...).


React Integration

import { useMemo } from 'react';
import { SpacetimeDBProvider, useTable } from 'spacetimedb/react';
import { DbConnection, tables } from './module_bindings';

function Root() {
  const connectionBuilder = useMemo(() =>
    DbConnection.builder()
      .withUri('ws://localhost:3000')
      .withDatabaseName('my_game')
      .withToken(localStorage.getItem('auth_token') || undefined)
      .onConnect((conn, identity, token) => {
        localStorage.setItem('auth_token', token);
        conn.subscriptionBuilder().subscribe(tables.player);
      }),
    []
  );

  return (
    <SpacetimeDBProvider connectionBuilder={connectionBuilder}>
      <App />
    </SpacetimeDBProvider>
  );
}

function PlayerList() {
  const [players, isReady] = useTable(tables.player);
  if (!isReady) return <div>Loading...</div>;
  return <ul>{players.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

Project Structure

Server (backend/spacetimedb/)

src/schema.ts   -> Tables, export spacetimedb
src/index.ts    -> Reducers, lifecycle, import schema
package.json    -> { "type": "module", "dependencies": { "spacetimedb": "^2.0.0" } }
tsconfig.json   -> Standard config

Client (client/)

src/module_bindings/ -> Generated (spacetime generate)
src/main.tsx         -> Provider, connection setup
src/App.tsx          -> UI components

Commands

spacetime start
spacetime publish <module-name> --module-path <backend-dir>
spacetime publish <module-name> --clear-database -y --module-path <backend-dir>
spacetime generate --lang typescript --out-dir <client>/src/module_bindings --module-path <backend-dir>
spacetime logs <module-name>

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

spacetimedb-typescript

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

spacetimedb-cli

No summary provided by upstream source.

Repository SourceNeeds Review
General

spacetimedb-concepts

No summary provided by upstream source.

Repository SourceNeeds Review