- 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
```rust
use spacetimedb::{
procedure, reducer, table, Filter, Identity, ProcedureContext, Query,
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:
```rust
#[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,
}
```
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 |
| `spacetimedb::sats::u256` / `spacetimedb::sats::i256` | 256-bit 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
```rust
#[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
```
Defaults support compatible addition of a newly appended field. Do not place `#[default(...)]` on primary-key, unique, or auto-increment columns.
## Indexes
Prefer `#[index(btree)]` inline for single-column. Multi-column uses table-level:
```rust
// 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))
```
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
```rust
#[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(())
}
```
Note: `insert()` panics on constraint violations. Use `try_insert()` with `?` when returning `Result`.
## DB Operations
```rust
ctx.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 column
```
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)`.
String column accessors operate on the column's owned `String` type, not `&str`. Pass a `String` or `&String` to `find` and `delete`; index filters borrow the key, as in `ctx.db.product().category().filter(&"hardware".to_string())`.
## Lifecycle Hooks
```rust
#[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) { ... }
```
The current connection ID is available through `ctx.connection_id()` (not a public field) and may be absent outside connection-scoped calls.
## Views
```rust
// 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())
}
```
Procedural-view table handles support indexed `find` and `filter` access, but not full-table `iter()`. Start a procedural view from an appropriate table index.
Declare a procedural view primary key in the view attribute:
```rust
#[view(accessor = catalog_entry, public, primary_key = sku)]
fn catalog_entry(ctx: &AnonymousViewContext) -> Vec<CatalogEntry> { ... }
```
Procedural-view primary keys are explicit schema metadata. Add one only when the view itself is required to expose a primary key; a source table's primary key is not inherited by the view.
Query-builder views use `ViewContext`, `ctx.from`, and return `impl Query<Row>`. Use `filter` for predicates and `right_semijoin` when the result should contain right-side rows that have a matching left-side row:
```rust
ctx.from.article().filter(|article| article.published.eq(true))
ctx.from.subscription().right_semijoin(
ctx.from.account(),
|subscription, account| subscription.account_id.eq(account.id),
)
```
Query-builder comparisons against a `String` column take an owned `String`, for example `row.label.eq("active".to_string())`.
Views can also accept arguments (2.13+), after the context:
```rust
#[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)
}
```
Floating-point arguments and primary keys are rejected on argument-bearing views. See client skills for SQL subscription syntax and restrictions.
## Client Visibility Filters
```rust
use spacetimedb::Filter;
#[spacetimedb::client_visibility_filter]
const PRIVATE_NOTE_FILTER: Filter =
Filter::Sql("SELECT * FROM owned_row WHERE owner = :sender");
```
## Reducer Context API
`ReducerContext` (`ctx`) is the only source of sender identity, time, and randomness; stdlib clocks and RNG are unavailable in modules.
```rust
// 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
```
## Environment Variables
```rust
#[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>
```
For constrained values, derive `spacetimedb::EnvironmentValue` on a payload-free enum and use it as a field type (or `Option<Enum>`). `#[env(value = "production")]` on a variant supplies its exact accepted string; without it, the Rust variant name is used. `#[env(values("info", "debug"))]` also constrains `String`/`Option<String>` fields.
See the concepts skill for shared environment and secret-handling rules.
## Scheduled Tables
```rust
#[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 });
```
Construct a connection ID from a numeric representation with `ConnectionId::from_u128(value)`.
## Inter-Database Calls (Async IDC)
A reducer calls a reducer in another database by inserting a row into an outbox table. The call is sent only if the transaction commits, runs exactly once, and runs in order with other calls through the same outbox to the same database. The sending reducer doesn't wait for it. Both databases must be on the same server.
Generate receiver bindings by listing the receiving module under `dependencies` in the sending database's `spacetime.json`, then running `spacetime generate <sending-database>`. Rust bindings are written to `src/remote_bindings/`.
```json
{
"database": "lobby",
"module-path": "./lobby",
"dependencies": [{ "name": "game_world", "database": "game-world" }],
"children": [{ "database": "game-world", "module-path": "./game_world" }]
}
```
```rust
Ver no GitHub