Writing SpacetimeDB Schema Definitions: The Complete Guide for Rust and TypeScript
SpacetimeDB schema definitions describe tables, columns, indexes, and scheduled reducers using either Rust procedural macros or TypeScript builder APIs, which compile to SATS-JSON schemas stored in the database's system tables.
In the clockworklabs/SpacetimeDB repository, modules declare their data model through schema definitions that bridge server-side storage and client-side type safety. Whether you are building multiplayer games or real-time collaboration tools, understanding how to write these definitions in both Rust and TypeScript is essential for leveraging SpacetimeDB's automatic migration and code generation capabilities.
Understanding SpacetimeDB Schema Architecture
SpacetimeDB employs a unified schema system where table metadata flows from source code into internal system tables (st_table, st_column, etc.). This architecture enables automatic schema migrations when you republish modules and guarantees type safety across the network boundary.
Core Schema Components
The implementation spans multiple layers of the codebase:
| Component | Role | Source Location |
|---|---|---|
#[spacetimedb::table] attribute |
Marks Rust structs as tables and generates WASM ABI metadata with options for public, primary_key, auto_inc, and index |
modules/sdk-test/src/lib.rs (lines 56-65) |
define_tables! macro |
Expands a compact DSL into multiple #[spacetimedb::table] structs plus associated reducer functions |
modules/sdk-test/src/lib.rs (lines 56-78) |
table() builder (TypeScript) |
Runtime API that registers table definitions and produces JSON-encoded SATS schemas | crates/bindings-typescript/src/server/schema.ts |
Column type helpers (t.identity(), t.u64()) |
TypeScript utilities defining column constraints including optional fields, primary keys, and schedule timestamps | crates/bindings-typescript/src/server/schema.ts (lines 104-119) |
| System tables | Internal metadata storage (st_table, st_column) that persists schema definitions and enables migration logic |
docs/versioned_docs/version-1.12.0/00300-resources/00200-reference/00400-sql-reference.md (line 494) |
How Schema Definitions Flow to the Database
- Authoring: Developers write Rust structs with
#[spacetimedb::table]attributes or use thedefine_tables!macro for bulk definitions. Alternatively, TypeScript modules use theschema()andtable()builders. - Compilation: The Rust compiler expands procedural macros into WASM-compatible metadata. TypeScript builders serialize to JSON at build time.
- Publishing: The
spacetime publishcommand transmits the compiled module and schema JSON to the server, which stores definitions in system tables and attempts automatic migration. - Codegen: The CLI generates client SDKs (
spacetime generate) that mirror the server schema, exposingspacetimedb.table(...)objects for type-checked client operations.
Defining Tables in Rust
Rust modules use attribute macros to declare tables, offering fine-grained control over visibility, indexing, and scheduling.
Using the #[spacetimedb::table] Attribute
The primary method for defining tables involves annotating structs with #[spacetimedb::table(...)]. In modules/sdk-test/src/lib.rs, this pattern implements a Users table with primary keys, secondary indexes, and public visibility:
#[spacetimedb::table(
accessor = users, // Client access path: ctx.db.users()
public, // Rows visible to all clients
primary_key, // The identity column serves as primary key
index(accessor = name_idx, btree(columns = [name]))
)]
pub struct Users {
#[primary_key]
identity: spacetimedb::Identity,
#[spacetimedb::index]
name: String,
}
Key arguments include:
- accessor: Defines the client-side API name for the table.
- public: Exposes all rows to connected clients without authentication checks.
- primary_key: Designates the struct's primary key column (required for unique row identification).
- index: Creates B-tree indexes on specified columns for query optimization.
Implementing Scheduled Tables
Scheduled tables trigger reducers automatically when their scheduled_at timestamp elapses. The following definition from the test suite demonstrates the scheduled argument linking a table to its handler reducer:
#[spacetimedb::table(
accessor = scheduled_message,
scheduled(send_scheduled_message), // Reducer invoked when schedule fires
public
)]
pub struct ScheduledMessage {
#[primary_key]
#[auto_inc] // Auto-generated u64 identifier
scheduled_id: u64,
scheduled_at: spacetimedb::ScheduleAt,
owner_identity: spacetimedb::Identity,
payload: String,
}
#[spacetime::reducer]
fn send_scheduled_message(ctx: &ReducerContext, msg: ScheduledMessage) {
// Business logic executes when scheduled_at is reached
ctx.db.users().insert(Users {
identity: msg.owner_identity,
name: msg.payload,
});
}
The #[auto_inc] attribute instructs the database to generate monotonically increasing IDs, while spacetimedb::ScheduleAt represents the timestamp type for scheduling.
Bulk Definitions with define_tables!
For modules requiring multiple similar tables, the define_tables! macro in modules/sdk-test/src/lib.rs (lines 77-89) provides a concise DSL that expands into complete table definitions and reducer functions:
define_tables! {
// Simple table with auto-generated insert reducer
OneU8 { insert insert_one_u8 } n u8;
// Table with unique constraint and custom reducers for update/delete
UniqueU32 {
insert_or_panic insert_unique_u32,
update_non_pk_by update_unique_u32 = update_by_n(n),
delete_by delete_unique_u32 = delete_by_n(n: u32),
} #[unique] n u32, data i32;
}
This macro generates structs annotated with #[spacetimedb::table] and implements the requested reducer signatures, supporting #[unique], #[primary_key], and auto_inc modifiers within the DSL syntax.
Defining Tables in TypeScript
The TypeScript SDK provides a programmatic builder API that mirrors Rust's declarative attributes, producing identical SATS-JSON schemas at runtime.
The table() Builder API
Located in crates/bindings-typescript/src/server/schema.ts, the table() function registers definitions with the server module. A real-world example from the chat application demo (tools/llm-oneshot/apps/chat-app/typescript/.../schema.ts) shows the equivalent Users table:
import { table, t } from 'spacetimedb/server';
export const Users = table(
{ name: 'users', public: true },
{
identity: t.identity().primaryKey(),
name: t.string().optional(),
online: t.bool(),
status: t.string(),
lastActive: t.timestamp(),
}
);
The first argument accepts metadata including name, public visibility, and optional scheduled reducer bindings. The second argument defines the column schema using type helpers.
Column Type Helpers and Constraints
The t namespace exposes methods for all SpacetimeDB primitive types with chained constraints:
export const ScheduledMessage = table(
{
name: 'scheduled_message',
public: true,
scheduled: 'send_scheduled_message',
},
{
scheduledId: t.u64().primaryKey().autoInc(),
scheduledAt: t.scheduleAt(),
ownerIdentity: t.identity(),
payload: t.string(),
}
);
Available chainable methods include:
.primaryKey(): Marks the column as the table's primary key..autoInc(): Enables automatic increment for integer columns..optional(): Makes the column nullable..scheduleAt(): Uses the special timestamp type for scheduled reducers.
Consuming Schemas in Client Bindings
When you run spacetime generate, the CLI reads the compiled module's schema and emits strongly-typed client code. The generated bindings expose table objects that match the server-side definitions exactly:
import { spacetimedb } from './module_bindings';
// TypeScript compiler verifies field names and types against the schema
await spacetimedb.users.insert({
identity: myIdentity,
name: 'Alice',
online: true,
status: 'online',
lastActive: new Date(),
});
// Schedule operations use helper methods corresponding to t.scheduleAt()
await spacetimedb.scheduled_message.insert({
scheduledAt: spacetimedb.ScheduleAt.fromNow(10_000),
ownerIdentity: myIdentity,
payload: 'Hello future!',
});
The client SDK references the same accessor names defined in the Rust attributes or TypeScript table() calls, ensuring the API surface remains consistent across language boundaries.
Summary
- SpacetimeDB schema definitions are authored in Rust using
#[spacetimedb::table]attributes or thedefine_tables!macro, and in TypeScript using thetable()builder API. - Core source files implementing this functionality include
modules/sdk-test/src/lib.rsfor Rust macro examples andcrates/bindings-typescript/src/server/schema.tsfor the TypeScript runtime. - Schema compilation produces SATS-JSON that the server stores in system tables (
st_table,st_column), enabling automatic migrations when modules are republished. - Advanced features such as
primary_key,auto_inc,index, andscheduledreducers are available in both languages through consistent APIs. - Client code generation creates type-safe bindings that mirror the server schema, preventing runtime errors through compile-time verification.
Frequently Asked Questions
What is the difference between #[spacetimedb::table] and define_tables! in Rust?
The #[spacetimedb::table] attribute marks individual structs as database tables with specific configuration options for visibility, indexing, and keys. In contrast, define_tables! is a macro that expands a domain-specific language into multiple table definitions and their associated reducer functions simultaneously, primarily used in the test suite for rapidly creating schema variations.
How do scheduled tables work in SpacetimeDB schema definitions?
Scheduled tables include a scheduled argument specifying a reducer name that executes automatically when the row's scheduled_at timestamp is reached. In Rust, use scheduled(reducer_name) in the table attribute with a column of type spacetimedb::ScheduleAt. In TypeScript, pass scheduled: 'reducer_name' to the table() options and use t.scheduleAt() for the column type. The server invokes the specified reducer with the row data when the schedule triggers.
Can I modify table schemas after publishing a SpacetimeDB module?
Yes, SpacetimeDB supports automatic schema migration when you republish a module with modified table definitions. The server compares the new schema JSON against existing system table metadata and attempts to migrate data to the new structure. However, destructive changes (such as removing columns or changing primary keys) may require manual intervention or data_reset flags depending on the compatibility of the changes.
Where are TypeScript schema definitions processed at runtime?
TypeScript schema definitions are processed by the table() and column helper functions defined in crates/bindings-typescript/src/server/schema.ts. These functions build a JSON representation of the schema that the SpacetimeDB server consumes during module initialization, effectively translating the TypeScript builder pattern into the same SATS format generated by Rust macros.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →