Develop SpacetimeDB server modules in Rust. Use when writing reducers, tables, or module logic.
SpacetimeDB modules are WebAssembly applications that run inside the database. They define tables to store data and reducers to modify data. Clients connect directly to the database and execute application logic inside it.
Tested with: SpacetimeDB 2.0+ APIs
These APIs/patterns are incorrect. LLMs frequently hallucinate them.
Both macro forms are valid in 2.0: #[spacetimedb::table(...)] / #[table(...)] and #[spacetimedb::reducer] / #[reducer].
#[derive(Table)] // Tables use #[table] attribute, not derive
#[derive(Reducer)] // Reducers use #[reducer] attribute
// WRONG — SpacetimeType on tables
#[derive(SpacetimeType)] // DO NOT use on #[table] structs!
#[table(accessor = my_table)]
pub struct MyTable { ... }
// WRONG — mutable context
pub fn my_reducer(ctx: &mut ReducerContext, ...) { } // Should be &ReducerContext
// WRONG — table access without parentheses
ctx.db.player // Should be ctx.db.player()
ctx.db.player.find(id) // Should be ctx.db.player().id().find(&id)
// WRONG — old 1.0 patterns
ctx.sender // Use ctx.sender() — method, not field (2.0)
.with_module_name("db") // Use .with_database_name() (2.0)
ctx.db.user().name().update(..) // Update only via primary key (2.0)
use spacetimedb::{table, reducer, Table, ReducerContext, Identity, Timestamp};
use spacetimedb::SpacetimeType; // Only for custom types, NOT tables
// CORRECT TABLE — accessor, not name; no SpacetimeType derive!
#[table(accessor = player, public)]
pub struct Player {
#[primary_key]
pub id: u64,
pub name: String,
}
// CORRECT REDUCER — immutable context, sender() is a method
#[reducer]
pub fn create_player(ctx: &ReducerContext, name: String) {
ctx.db.player().insert(Player { id: 0, name });
}
// CORRECT TABLE ACCESS — methods with parentheses, sender() method
let player = ctx.db.player().id().find(&player_id);
let caller = ctx.sender();
SpacetimeType on #[table] structs — the macro handles this&ReducerContext, not &mut ReducerContextTable trait import — required for table operationsctx.db.player() not ctx.db.playerctx.sender — it's ctx.sender() (method) in 2.0| Wrong | Right | Error |
|---|---|---|
#[table(accessor = "my_table")] | #[table(accessor = my_table)] | String literals not allowed |
Missing public on table | Add public flag | Clients can't subscribe |
| Network/filesystem in reducer | Use procedures instead | Sandbox violation |
| Panic for expected errors | Return Result<(), String> | WASM instance destroyed |
SpacetimeType on #[table] structs — the macro handles thisTable trait — required for all table operations&ReducerContext — not &mut ReducerContextctx.db.table() not ctx.db.tablectx.sender() — method call, not field access (2.0)accessor = for API handles — name = "..." is optional canonical naming in table/index attributesctx.rng() — not rand crate for random numberspublic flag — if clients need to subscribe to a table[package]
name = "my-module"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb = { workspace = true }
log = "0.4"
use spacetimedb::{ReducerContext, Table};
use spacetimedb::{Identity, Timestamp, ConnectionId, ScheduleAt};
#[spacetimedb::table(accessor = player, public)]
pub struct Player {
#[primary_key]
#[auto_inc]
id: u64,
name: String,
score: u32,
}
| Attribute | Description |
|---|---|
accessor = identifier | Required. The API name used in ctx.db.{accessor}() |
public | Makes table visible to clients via subscriptions |
scheduled(function_name) | Creates a schedule table that triggers the named reducer or procedure |
index(accessor = idx, btree(columns = [a, b])) | Multi-column index |
| Attribute | Description |
|---|---|
#[primary_key] | Unique identifier for the row (one per table max) |
#[unique] | Enforces uniqueness, enables find() method |
#[auto_inc] | Auto-generates unique integer values when inserting 0 |
#[index(btree)] | Creates a B-tree index for efficient lookups |
Primitives: u8-u256, i8-i256, f32, f64, bool, String
SpacetimeDB Types: Identity, ConnectionId, Timestamp, Uuid, ScheduleAt
Collections: Vec<T>, Option<T>, Result<T, E>
Custom Types: Any struct/enum with #[derive(SpacetimeType)]
#[spacetimedb::reducer]
pub fn create_player(ctx: &ReducerContext, name: String) -> Result<(), String> {
if name.is_empty() {
return Err("Name cannot be empty".to_string());
}
ctx.db.player().insert(Player { id: 0, name, score: 0 });
Ok(())
}
&ReducerContext(), Result<(), String>, or Result<(), E> where E: DisplayErr returnTable trait: use spacetimedb::Table;ctx.db // Database access
ctx.sender() // Identity of the caller (method, not field!)
ctx.connection_id() // Option<ConnectionId> (None for scheduled/system reducers)
ctx.timestamp // Invocation timestamp
ctx.identity() // Module's own identity
ctx.rng() // Deterministic RNG (method, not field!)
// Insert returns the row with auto_inc values populated
let player = ctx.db.player().insert(Player { id: 0, name: "Alice".into(), score: 100 });