Skip to content

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 typesAdminRoleLevel, 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:

spacetimedb publish --project-path . qs-bridge-platform

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:

  1. Captures the Identity of the caller who published the module (ctx.sender()).
  2. Stores it as owner_identity in the ModuleConfig singleton table.
  3. 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:

  1. The init reducer does not re-fire. The existing ModuleConfig singleton (and all other data) is preserved.
  2. Schema changes follow SpacetimeDB's migration rules — new columns with defaults are allowed; destructive changes require explicit migration.
  3. 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.