Skip to main content

rust-server

SpacetimeDB Rust server module SDK reference. Use when writing tables, reducers, or module logic in Rust.

ソース情報

リポジトリ
clockworklabs/SpacetimeDB
ソースの最終更新活動
2026年8月15日 01:50
検出された SKILL.md の言語
英語
スター
25,387
フォーク
1,096

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
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())`. ## 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 ``` ## 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)`. ## Procedures and HTTP Procedures use `&mut ProcedureContext` and may return typed values: ```rust use 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 } } ``` Outbound HTTP is available through `ctx.http`. Convenience methods such as `get` return a response, and other methods use `Request::builder()` with `ctx.http.send(request)`. `response.status()` returns a `StatusCode`; use `is_success()` to test it or `as_u16()` when a numeric status is needed. Responses are not cloneable, so inspect status and headers before consuming the body with `response.into_body().into_string_lossy()`. Header values use the fallible `to_str()` conversion; they do not provide `as_str()`. Open short database transactions with `ctx.with_tx(|tx| ...)`. Access tables inside the callback through `tx.db`, not directly on `tx`. It returns the callback's value directly, so do not call `unwrap` or `expect` on the result unless the callback itself returns a `Result`. The callback implements `Fn`, so clone captured owned values when storing them rather than moving them out of the closure. Perform network I/O before opening the transaction. For an outbound request without a convenience method: ```rust let request = Request::builder() .method("POST") .uri(url) .body(Body::from_bytes(data)) .unwrap(); let response = ctx.http.send(request).expect("request failed"); ``` Scheduled procedures use the ordinary scheduled-table shape, but the scheduled function is marked `#[procedure]` and receives `&mut ProcedureContext` plus the scheduled row. Inbound HTTP uses handler functions and one router: ```rust use 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]
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る