| name | rust-server |
| description | SpacetimeDB Rust server module SDK reference. Use when writing tables, reducers, or module logic in Rust. |
| license | Apache-2.0 |
| metadata | {"author":"clockworklabs","version":"2.0","role":"server","language":"rust","cursor_globs":"**/*.rs","cursor_always_apply":true} |
SpacetimeDB Rust SDK Reference
Imports
use spacetimedb::{
reducer, table, Identity, ReducerContext, SpacetimeType, Table,
ConnectionId, ScheduleAt, TimeDuration, Timestamp, Uuid,
};
Table is required. Without it, ctx.db.*.insert(), .iter(), .find() etc. won't compile (no method named 'insert' found).
Tables
#[spacetimedb::table(...)] on a pub struct. accessor must be snake_case:
#[spacetimedb::table(accessor = entity, public)]
pub struct Entity {
#[primary_key]
#[auto_inc]
pub id: u64,
pub owner: Identity,
pub name: String,
pub active: bool,
}
Options: accessor = snake_case (required), public, scheduled(reducer_fn), index(...)
ctx.db accessors use the accessor name (snake_case).
Column Types
| Rust type | Notes |
|---|
u8 / u16 / u32 / u64 / u128 | unsigned integers |
i8 / i16 / i32 / i64 / i128 | signed integers |
f32 / f64 | floats |
bool | boolean |
String | text |
Vec<T> | list/array |
Identity | user identity |
ConnectionId | connection handle |
Timestamp | server timestamp (microseconds since epoch) |
TimeDuration | duration in microseconds |
Uuid | UUID |
Option<T> | nullable column |
Column Attributes
#[primary_key]
#[auto_inc]
#[unique]
#[index(btree)]
Indexes
Prefer #[index(btree)] inline for single-column. Multi-column uses table-level:
#[index(btree)]
pub author_id: u64,
#[spacetimedb::table(accessor = membership, public,
index(accessor = by_group_user, btree(columns = [group_id, user_id]))
)]
pub struct Membership { pub group_id: u64, pub user_id: Identity, ... }
When you frequently look up rows by multiple columns, prefer a multi-column index over filtering by one column and looping over the results.
Reducers
#[spacetimedb::reducer]
pub fn create_entity(ctx: &ReducerContext, name: String) {
ctx.db.entity().insert(Entity { id: 0, owner: ctx.sender(), name, active: true });
}
#[spacetimedb::reducer]
pub fn validate_entity(ctx: &ReducerContext, name: String) -> Result<(), String> {
if name.is_empty() {
return Err("Name cannot be empty".to_string());
}
ctx.db.entity().try_insert(Entity { id: 0, owner: ctx.sender(), name, active: true })?;
Ok(())
}
Note: insert() panics on constraint violations. Use try_insert() with ? when returning Result.
DB Operations
ctx.db.entity().insert(Entity { id: 0, name: "Sample".into() });
ctx.db.entity().id().find(entity_id);
ctx.db.entity().identity().find(ctx.sender());
ctx.db.item().author_id().filter(author_id);
ctx.db.entity().iter();
ctx.db.entity().count();
ctx.db.entity().id().update(Entity { ..existing, name: new_name });
ctx.db.entity().id().delete(entity_id);
ctx.db.entity().name().delete("Alice");
Note: iter() and filter() return iterators. Collect to Vec if you need .sort(), .filter(), .map().
Range queries on btree indexes: filter(18..=65), filter(18..), filter(..18).
Lifecycle Hooks
#[spacetimedb::reducer(init)]
pub fn init(ctx: &ReducerContext) { ... }
#[spacetimedb::reducer(client_connected)]
pub fn on_connect(ctx: &ReducerContext) { ... }
#[spacetimedb::reducer(client_disconnected)]
pub fn on_disconnect(ctx: &ReducerContext) { ... }
Views
use spacetimedb::{view, AnonymousViewContext};
#[view(accessor = active_users, public)]
fn active_users(ctx: &AnonymousViewContext) -> Vec<Entity> {
ctx.db.entity().iter().filter(|e| e.active).collect()
}
use spacetimedb::{view, ViewContext};
#[view(accessor = my_profile, public)]
fn my_profile(ctx: &ViewContext) -> Option<Entity> {
ctx.db.entity().identity().find(ctx.sender())
}
Reducer Context API
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.
if row.owner != ctx.sender() {
panic!("unauthorized");
}
ctx.db.item().insert(Item { id: 0, owner: ctx.sender(), created_at: ctx.timestamp, .. });
let expiry = ctx.timestamp + TimeDuration::from_micros(delay_micros);
use spacetimedb::rand::Rng;
let n: u32 = ctx.random();
let roll: u32 = ctx.rng().gen_range(1..=6);
timestamp.to_micros_since_unix_epoch() / 1000
Scheduled Tables
#[spacetimedb::table(accessor = tick_timer, scheduled(tick), public)]
pub struct TickTimer {
#[primary_key]
#[auto_inc]
scheduled_id: u64,
scheduled_at: spacetimedb::ScheduleAt,
}
#[spacetimedb::reducer]
pub fn tick(ctx: &ReducerContext, timer: TickTimer) {
}
let at = ScheduleAt::Time(ctx.timestamp + std::time::Duration::from_secs(10));
let at = ScheduleAt::Interval(std::time::Duration::from_secs(5).into());
ctx.db.tick_timer().insert(TickTimer { scheduled_id: 0, scheduled_at: at });
Logging
log::info!("Player connected: {:?}", ctx.sender());
log::warn!("Low health: {}", hp);
log::error!("Failed to find entity");
Custom Types
#[derive(SpacetimeType)]
pub enum Status { Online, Away, Offline }
#[derive(SpacetimeType)]
pub struct Point { x: f32, y: f32 }
Complete Example
use spacetimedb::{Identity, ReducerContext, SpacetimeType, Table, Timestamp};
#[spacetimedb::table(accessor = entity, public)]
pub struct Entity {
#[primary_key]
pub identity: Identity,
pub name: String,
pub active: bool,
}
#[spacetimedb::table(accessor = record, public)]
pub struct Record {
#[primary_key]
#[auto_inc]
pub id: u64,
pub owner: Identity,
pub value: u32,
pub created_at: Timestamp,
}
#[spacetimedb::reducer(client_connected)]
pub fn on_connect(ctx: &ReducerContext) {
if let Some(existing) = ctx.db.entity().identity().find(ctx.sender()) {
ctx.db.entity().identity().update(Entity { active: true, ..existing });
}
}
#[spacetimedb::reducer(client_disconnected)]
pub fn on_disconnect(ctx: &ReducerContext) {
if let Some(existing) = ctx.db.entity().identity().find(ctx.sender()) {
ctx.db.entity().identity().update(Entity { active: false, ..existing });
}
}
#[spacetimedb::reducer]
pub fn create_entity(ctx: &ReducerContext, name: String) {
if ctx.db.entity().identity().find(ctx.sender()).is_some() {
panic!("already exists");
}
ctx.db.entity().insert(Entity { identity: ctx.sender(), name, active: true });
}
#[spacetimedb::reducer]
pub fn add_record(ctx: &ReducerContext, value: u32) {
if ctx.db.entity().identity().find(ctx.sender()).is_none() {
panic!("not found");
}
ctx.db.record().insert(Record {
id: 0,
owner: ctx.sender(),
value,
created_at: ctx.timestamp,
});
}