Loading...
Loading...
SpacetimeDB Rust server module SDK reference. Use when writing tables, reducers, or module logic in Rust.
npx skill4agent add clockworklabs/spacetimedb rust-serveruse spacetimedb::{
procedure, reducer, table, Filter, Identity, ProcedureContext, Query,
ReducerContext, SpacetimeType, Table, ConnectionId, ScheduleAt,
TimeDuration, Timestamp, Uuid,
};Tablectx.db.*.insert().iter().find()no method named 'insert' found#[spacetimedb::table(...)]pub structaccessor#[spacetimedb::table(accessor = entity, public)]
pub struct Entity {
#[primary_key]
#[auto_inc]
pub id: u64,
pub owner: Identity,
pub name: String,
#[index(btree)]
pub active: bool,
}accessor = snake_casepublicscheduled(reducer_fn)index(...)ctx.dbaccessor| Rust type | Notes |
|---|---|
| unsigned integers |
| signed integers |
| 256-bit integers |
| floats |
| boolean |
| text |
| list/array |
| user identity |
| connection handle |
| server timestamp (microseconds since epoch) |
| duration in microseconds |
| UUID |
| nullable column |
#[primary_key] // primary key
#[auto_inc] // auto-increment (use 0 as placeholder on insert)
#[unique] // unique constraint
#[index(btree)] // btree index (enables .filter() on this column)
#[default(true)] // migration-safe default for a newly appended column#[default(...)]#[index(btree)]// Inline (preferred for single-column):
#[index(btree)]
pub author_id: u64,
// Access: ctx.db.post().author_id().filter(author_id)
// Multi-column (table-level):
#[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, ... }
// Access: ctx.db.membership().by_group_user().filter((group_id, &user_id))#[spacetimedb::reducer]
pub fn create_entity(ctx: &ReducerContext, name: String) {
ctx.db.entity().insert(Entity { id: 0, owner: ctx.sender(), name, active: true });
}
// Reducers can return Result<(), String> or Result<(), E> where E: Display
#[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(())
}insert()try_insert()?Resultctx.db.entity().insert(Entity { id: 0, name: "Sample".into() }); // Insert (0 for autoInc)
ctx.db.entity().id().find(entity_id); // Find by PK → Option<Entity>
ctx.db.entity().identity().find(ctx.sender()); // Find by unique column → Option<Entity>
ctx.db.item().author_id().filter(author_id); // Filter by index → iterator
ctx.db.entity().iter(); // All rows → iterator
ctx.db.entity().count(); // Count rows
ctx.db.entity().id().update(Entity { name: new_name, ..existing }); // Update (override + spread)
ctx.db.entity().id().delete(entity_id); // Delete by PK
ctx.db.entity().name().delete("Alice".to_string()); // Delete by indexed String columniter()filter().sort().filter().map()filter(18..=65)filter(18..)filter(..18)String&strString&Stringfinddeletectx.db.product().category().filter(&"hardware".to_string())#[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) { ... }ctx.connection_id()// Anonymous view (same result for all clients):
use spacetimedb::{view, AnonymousViewContext};
#[view(accessor = active_users, public)]
fn active_users(ctx: &AnonymousViewContext) -> Vec<Entity> {
ctx.db.entity().active().filter(true).collect()
}
// Per-user view (result varies by sender):
use spacetimedb::{view, ViewContext};
#[view(accessor = my_profile, public)]
fn my_profile(ctx: &ViewContext) -> Option<Entity> {
ctx.db.entity().identity().find(ctx.sender())
}findfilteriter()#[view(accessor = catalog_entry, public, primary_key = sku)]
fn catalog_entry(ctx: &AnonymousViewContext) -> Vec<CatalogEntry> { ... }ViewContextctx.fromimpl Query<Row>filterright_semijoinctx.from.article().filter(|article| article.published.eq(true))
ctx.from.subscription().right_semijoin(
ctx.from.account(),
|subscription, account| subscription.account_id.eq(account.id),
)StringStringrow.label.eq("active".to_string())#[spacetimedb::view(accessor = entity_by_id, public)]
pub fn entity_by_id(ctx: &spacetimedb::AnonymousViewContext, id: u64) -> Option<Entity> {
ctx.db.entity().id().find(id)
}use spacetimedb::Filter;
#[spacetimedb::client_visibility_filter]
const PRIVATE_NOTE_FILTER: Filter =
Filter::Sql("SELECT * FROM owned_row WHERE owner = :sender");ReducerContextctx// Auth: ctx.sender() is the caller's Identity
if row.owner != ctx.sender() {
panic!("unauthorized");
// or: return Err(anyhow::anyhow!("unauthorized"));
}
// Server timestamp (deterministic per reducer call)
ctx.db.item().insert(Item { id: 0, owner: ctx.sender(), created_at: ctx.timestamp, .. });
// Timestamp arithmetic
let expiry = ctx.timestamp + TimeDuration::from_micros(delay_micros);
// Deterministic RNG: ctx.random() for a single value, ctx.rng() for the rand::Rng trait
use spacetimedb::rand::Rng;
let n: u32 = ctx.random(); // random u32
let roll: u32 = ctx.rng().gen_range(1..=6); // any rand::Rng method
// Client: Timestamp → milliseconds since epoch
timestamp.to_micros_since_unix_epoch() / 1000#[spacetimedb::env]
pub struct Env {
pub API_KEY: String,
pub LOG_LEVEL: Option<String>,
}
// Inside a root entry point:
// ctx.env.API_KEY() -> String
// ctx.env.LOG_LEVEL() -> Option<String>
// ctx.env.get("LOG_LEVEL") -> Option<String>spacetimedb::EnvironmentValueOption<Enum>#[env(value = "production")]#[env(values("info", "debug"))]StringOption<String>#[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) {
// timer row is auto-deleted after this reducer runs
}
// One-time: fires once at a specific time
let at = ScheduleAt::Time(ctx.timestamp + std::time::Duration::from_secs(10));
// Repeating: fires on an interval
let at = ScheduleAt::Interval(std::time::Duration::from_secs(5).into());
ctx.db.tick_timer().insert(TickTimer { scheduled_id: 0, scheduled_at: at });ConnectionId::from_u128(value)dependenciesspacetime.jsonspacetime generate <sending-database>src/remote_bindings/{
"database": "lobby",
"module-path": "./lobby",
"dependencies": [{ "name": "game_world", "database": "game-world" }],
"children": [{ "database": "game-world", "module-path": "./game_world" }]
}mod remote_bindings;
use remote_bindings::game_world;
// Each row is one call to game_world's `admit_player(name: String)` reducer.
#[spacetimedb::table(accessor = admit_player_outbox, outbox(game_world::admit_player))]
pub struct AdmitPlayerOutbox {
#[primary_key]
#[auto_inc]
msg_id: u64, // exactly one u64 auto_inc primary key
#[target]
world: game_world::Identity, // exactly one target, typed by the bindings
name: String, // one column per reducer parameter, same name and type
// #[param(name = name)] player_name: String, // alias a column to a parameter
// Extra columns are allowed. They aren't sent, but on_result can read them.
}
#[spacetimedb::reducer]
pub fn send_to_world(ctx: &ReducerContext, name: String, world: game_world::Identity) {
// Sent if and only if this transaction commits.
ctx.db.admit_player_outbox().insert(AdmitPlayerOutbox { msg_id: 0, world, name });
}
// Optional, at most one per outbox. Private. Runs once per row, then the row is deleted.
#[spacetimedb::reducer(on_result(admit_player_outbox))]
pub fn on_admit_player_result(ctx: &ReducerContext, row: AdmitPlayerOutbox, result: Result<(), String>) {
if let Err(error) = result {
log::warn!("{} was not admitted: {error}", row.name);
}
}ctx.sender()client_connectedclient_disconnectedclient_connectedon_resultIdentitygame_world::Identity(identity)findfiltergame_world::Identity&mut ProcedureContextuse spacetimedb::{procedure, ProcedureContext, SpacetimeType};
#[derive(SpacetimeType)]
pub struct ResultValue { pub value: String }
#[procedure]
pub fn inspect(_ctx: &mut ProcedureContext, input: String) -> ResultValue {
ResultValue { value: input }
}ctx.httpgetRequest::builder()ctx.http.send(request)response.status()StatusCodeis_success()as_u16()response.into_body().into_string_lossy()to_str()as_str()ctx.with_tx(|tx| ...)tx.dbtxunwrapexpectResultFnlet request = Request::builder()
.method("POST")
.uri(url)
.body(Body::from_bytes(data))
.unwrap();
let response = ctx.http.send(request).expect("request failed");#[procedure]&mut ProcedureContextuse spacetimedb::http::{handler, router, Body, HandlerContext, Request, Response, Router};
#[handler]
fn health(_ctx: &mut HandlerContext, _request: Request) -> Response {
Response::builder().status(200).body(Body::from_bytes("ok")).unwrap()
}
#[router]
fn routes() -> Router { Router::new().get("/health", health) }HandlerContextdbctx.with_tx(|tx| ...)'staticStringBody::from_byteslog::info!("Player connected: {:?}", ctx.sender());
log::warn!("Low health: {}", hp);
log::error!("Failed to find entity");#[derive(SpacetimeType)]
pub enum Status { Online, Away, Offline }
#[derive(SpacetimeType)]
pub struct Point { x: f32, y: f32 }// src/lib.rs
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,
});
}spacetimedbtest-utils[dependencies]
spacetimedb = { version = "2.13.0", features = ["unstable"] }
[dev-dependencies]
spacetimedb = { version = "2.13.0", features = ["test-utils"] }
icu_calendar = ">=2.1, <2.2" # temporal_rs requires icu_calendar 2.1.xcargo testcreate_entity#[cfg(test)]
mod tests {
use super::*;
use spacetimedb::test_utils::{TestAuth, TestContext};
#[test]
fn inserts_entity() {
let test = TestContext::new().expect("test database");
test.with_reducer_tx(TestAuth::internal(), |ctx| {
create_entity(ctx, "Alice".to_owned());
Ok::<(), String>(())
}).expect("transaction");
assert_eq!(test.db.entity().count(), 1);
}
}with_reducer_txOkErr?ResultTestContextTestAuth::from_jwt_payload(payload, connection_id)?TestAuth::internal()clockrng