SpacetimeDB Module¶
The Rust WASM module powering QS-Bridge — 12 tables, 24 reducers, deployed to SpacetimeDB.
The QS-Bridge platform is implemented as a Rust WASM module deployed to SpacetimeDB, a relational database designed for real-time multiplayer applications. This page documents the module's structure, initialization process, and security architecture.
Module Structure¶
The platform module is a single Rust crate compiled to WebAssembly (WASM) and published to a SpacetimeDB instance. It contains:
- 12 table definitions — annotated with
#[spacetimedb::table] - 24 reducer functions — annotated with
#[spacetimedb::reducer] - Enum types —
AdminRoleLevel,AdminLevel, and transfer/event status enums - Guard functions — Internal authorization helpers like
is_gateway()
// Crate-level structure (simplified)
use spacetimedb::{table, reducer, Identity, ReducerContext, Timestamp};
mod tables; // All 12 table definitions
mod reducers; // All 24 reducer functions
mod guards; // Authorization logic
mod types; // Shared enums and type aliases
Compilation Target¶
The module compiles to wasm32-unknown-unknown and is deployed using the SpacetimeDB CLI:
Once published, the module runs inside SpacetimeDB's WASM runtime. All reducer calls are serialized, transactional, and executed in order — there are no race conditions or concurrent mutation conflicts.
Initialization¶
SpacetimeDB modules support a special init reducer that fires exactly once when the module is first published. QS-Bridge uses this to establish module ownership.
The init Reducer¶
#[spacetimedb::reducer(init)]
pub fn init(ctx: &ReducerContext) {
ModuleConfig::insert(ModuleConfig {
owner_identity: ctx.sender(),
});
}
This reducer:
- Captures the
Identityof the caller who published the module (ctx.sender()). - Stores it as
owner_identityin the ModuleConfig singleton table. - Never runs again — even across module upgrades.
The owner_identity value becomes the root of trust for the entire platform. It represents the hosting operator or deployment automation that controls the SpacetimeDB instance.
Why ctx.sender()?¶
In SpacetimeDB, ctx.sender() returns the Identity of the client (or CLI tool) that invoked the reducer. During init, this is the identity of whoever ran spacetimedb publish. This identity is cryptographic — it cannot be forged or impersonated.
The is_gateway() Guard¶
The is_gateway() function is the platform's primary authorization gate. It determines whether a reducer call originates from the trusted gateway (the module owner) or from an untrusted client.
Implementation¶
fn is_gateway(ctx: &ReducerContext) -> bool {
let config = ModuleConfig::filter_iter()
.next()
.expect("ModuleConfig must exist");
ctx.sender() == config.owner_identity
}
Usage Pattern¶
Most admin and panel reducers begin with an is_gateway() check:
#[spacetimedb::reducer]
pub fn admin_ban(ctx: &ReducerContext, player_id: String, reason: String) {
if !is_gateway(ctx) {
log::warn!("Unauthorized admin_ban attempt from {:?}", ctx.sender());
return;
}
// ... proceed with ban logic
}
Security Model¶
The is_gateway() guard enforces a simple but effective trust boundary:
graph TD
subgraph STDB["SpacetimeDB"]
subgraph Module["QS-Bridge Module"]
Check["is_gateway()"]
Check -->|"ctx.sender() == owner_identity?"| Decision{" "}
Decision -->|YES| Proceed["proceed"]
Decision -->|NO| Reject["reject"]
end
end
| Caller | is_gateway() Result |
Access |
|---|---|---|
| Deployment CLI / Gateway service | true |
Full admin access |
| Game server (via SDK) | false |
Denied unless reducer allows it |
| Player client (direct) | false |
Denied |
| Admin panel (via gateway proxy) | true |
Full admin access (proxied) |
The admin panel does not connect directly to SpacetimeDB with its own identity. Instead, it communicates through a gateway service that holds the owner_identity credentials. This means the panel's requests arrive as the gateway identity, passing the is_gateway() check. Panel-specific authorization (verifying the admin's role level) happens in the panel reducer layer — see Reducers.
Reducer Execution Model¶
SpacetimeDB reducers execute with serializable isolation:
- Single-threaded: Only one reducer runs at a time per module.
- Transactional: Each reducer call is an atomic transaction. If it panics, all changes are rolled back.
- Ordered: Reducers execute in the order they are received by the SpacetimeDB instance.
This model eliminates entire classes of concurrency bugs. Operations like "check if player is banned, then allow connection" are inherently safe — no other reducer can modify the ban list between the check and the action.
Error Handling¶
Reducers in QS-Bridge follow a guard-and-return pattern rather than panicking:
#[spacetimedb::reducer]
pub fn admin_set_motd(ctx: &ReducerContext, server_id: String, motd: String) {
if !is_gateway(ctx) {
return; // Silent rejection
}
if let Some(mut config) = ServerConfig::filter_by_server_id(&server_id) {
config.motd = motd;
ServerConfig::update_by_server_id(&server_id, config);
}
// If server_id not found, no-op
}
This approach avoids transaction rollbacks on expected conditions (unauthorized calls, missing records) while still allowing panics for truly exceptional situations (missing ModuleConfig singleton).
Module Upgrades¶
SpacetimeDB supports in-place module upgrades via spacetimedb publish. During an upgrade:
- The
initreducer does not re-fire. The existingModuleConfigsingleton (and all other data) is preserved. - Schema changes follow SpacetimeDB's migration rules — new columns with defaults are allowed; destructive changes require explicit migration.
- Active subscriptions are re-evaluated against the new schema.
This means the owner_identity established during initial deployment persists across all future upgrades, maintaining a consistent root of trust.
Related Pages¶
- Platform Overview — High-level architecture and design principles
- ModuleConfig Table — Singleton table storing the owner identity
- Reducers — All 24 reducers with auth requirements
- Multi-Server Architecture — How the gateway identity enables cross-server operations