How to Define SpacetimeDB Tables: Rust Macros and TypeScript SDK Guide
SpacetimeDB tables are defined using the #[spacetimedb::table] attribute macro in Rust or the table() function in TypeScript, both of which register schemas with the runtime while generating type-safe accessors for queries and reducers.
SpacetimeDB tables persist rows inside the database and serve as the foundation for reactive multiplayer applications. In the clockworklabs/SpacetimeDB repository, you define these tables declaratively using compile-time macros in Rust or schema-building functions in TypeScript. Understanding how to define SpacetimeDB tables correctly ensures your data layer generates efficient indexes and accessible APIs for client SDKs.
Rust Table Definitions via Procedural Macros
The Rust SDK provides two primary ways to declare tables: the standard attribute macro for production code and a convenience macro for rapid testing.
Using the #[spacetimedb::table] Attribute
The canonical approach uses the procedural macro defined in crates/spacetimedb-macros/src/table.rs. Apply #[spacetimedb::table(...)] to a pub struct to register it with the database runtime and generate accessor methods.
#[spacetimedb::table(
accessor = person,
public,
index(accessor = age_index, btree(columns = [age]))
)]
pub struct Person {
#[primary_key]
id: u64,
name: String,
age: u32,
}
The macro expands your struct to implement the spacetimedb::table::Table trait. The accessor parameter defines the method name used to reach the table from reducer contexts (e.g., ctx.db.person()). The public flag makes the table readable by any connected client, while private restricts access to server-side reducers. You can declare multi-column B-tree indexes using the index option to optimize query performance.
Additional configuration options include:
scheduled(<reducer>)– Automatically invokes the specified reducer when a row's timestamp column is reachedevent– Creates an event table where rows are emitted as transient events rather than persisted state
See the real-world implementation in demo/Blackholio/server-rust/src/lib.rs, which demonstrates public, private, scheduled, and event table definitions side-by-side.
Convenience Testing with define_tables!
For rapid prototyping and integration tests, the repository provides the define_tables! macro located in modules/sdk-test/src/lib.rs (lines 55-73). This macro simultaneously declares a table and generates basic reducers for CRUD operations.
define_tables! {
Person {
insert insert_person,
delete delete_person,
update_by update_person = update_by(id)
}
#[primary_key] id: u64,
name: String,
age: u32,
}
The macro expands to a #[spacetimedb::table] struct definition plus reducer functions matching the signatures you specify. For example, insert insert_person generates a reducer that accepts the field types and calls ctx.db.person().insert(Person { ... }). This scaffolding appears frequently in the Rust test client at sdks/rust/tests/test-client/src/simple_test_table.rs.
TypeScript Table Definitions
When using the TypeScript SDK, tables are defined programmatically using the table function exported from crates/bindings-typescript/src/lib/table.ts (lines 7-22).
The table() Schema Builder
Import table and the type builders from spacetimedb/server to construct a TableSchema object that the runtime uses to create the table:
import { table, t } from "spacetimedb/server";
export const schema = table(
{
name: "user",
public: true,
indexes: [
{ accessor: "email_index", btree: { columns: ["email"] } },
],
},
{
id: t.u64().primaryKey(),
email: t.string(),
name: t.string(),
}
);
The function performs compile-time validation of column constraints. For instance, it prevents conflicting declarations like default() combined with primaryKey(), emitting descriptive type errors before runtime. The returned schema object provides methods for inserting rows and executing queries within your reducer logic.
Advanced Configuration Options
Both SDKs support sophisticated table behaviors beyond simple row storage.
Multi-Column Indexes and Visibility
Define composite indexes to accelerate queries filtering on multiple fields:
#[spacetimedb::table(
accessor = log,
index(accessor = timestamp_user_index, btree(columns = [timestamp, user_id]))
)]
pub struct Log {
#[primary_key]
id: u64,
user_id: u64,
timestamp: u64,
message: String,
}
In TypeScript, pass an array of index definitions to the indexes option using the same btree configuration object.
Scheduled Reducers
Register tables with the runtime scheduler to trigger reducers based on temporal conditions:
import { table, t, schedule } from "spacetimedb/server";
export const taskTable = table(
{
name: "task",
public: true,
scheduled: () => schedule.send_scheduled_message,
},
{
id: t.u64().primaryKey(),
description: t.string(),
due_at: t.timestamp(),
}
);
When a row's due_at timestamp is reached, the runtime automatically invokes the send_scheduled_message reducer with the row data. The Rust equivalent uses the scheduled(<reducer_name>) option within the #[spacetimedb::table] attribute.
Summary
- Rust attribute macro: Use
#[spacetimedb::table(...)]on structs, implemented incrates/spacetimedb-macros/src/table.rs, to define tables with compile-time code generation. - Rust test macro: The
define_tables!macro inmodules/sdk-test/src/lib.rsgenerates both tables and reducers for testing scenarios. - TypeScript function: Call
table(opts, rowDef)fromcrates/bindings-typescript/src/lib/table.tsto create validated table schemas. - Primary keys: Required for update operations, declared with
#[primary_key]in Rust or.primaryKey()in TypeScript. - Indexes: Define multi-column B-tree indexes declaratively in both languages to optimize query paths.
- Visibility: Mark tables
publicto expose them to clients or omit the flag (Rust) or setfalse(TypeScript) for private server-side storage.
Frequently Asked Questions
What is the difference between public and private tables in SpacetimeDB?
Public tables allow any connected client to read their contents through the client SDK, while private tables are only accessible from server-side reducers within the same module. In Rust, add the public argument to the attribute macro; in TypeScript, set public: true in the table options object.
Can I define multi-column indexes in SpacetimeDB?
Yes. In Rust, use index(accessor = <name>, btree(columns = [<col1>, <col2>])) inside the #[spacetimedb::table] attribute. In TypeScript, include an object with btree: { columns: ["col1", "col2"] } in the indexes array. These indexes optimize queries that filter on the specified column combinations.
Where are the table definition macros implemented in the SpacetimeDB source code?
The Rust procedural macro is implemented in crates/spacetimedb-macros/src/table.rs, which parses the attribute arguments and generates the accessor methods. The TypeScript table function lives in crates/bindings-typescript/src/lib/table.ts and handles runtime schema validation and registration.
How do I create a table that automatically triggers reducers at specific times?
Use the scheduled table feature. In Rust, add scheduled(<reducer_name>) to your #[spacetimedb::table] arguments. In TypeScript, include scheduled: () => schedule.<your_reducer> in the table options. The runtime monitors the specified timestamp column and invokes the reducer when each row's time is reached.
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 →