SpacetimeDB

website·Indexed 34 days ago

https://staging.spacetimedb.com/

A relational database system that integrates application logic directly within the database. Documentation covers core concepts, development workflows, deployment, and technical references for identities, reducers, and managing tables via WASM instances.

Tokens
16.7K
Snippets
52
Records
69
Agent score
0%

What's inside SpacetimeDB

  1. Overview of SpacetimeDB Architecture

    SpacetimeDB is a relational database system that integrates application logic directly within the database using WebAssembly (WASM) modules, removing the need for separate web or game servers. It is optimized for high-throughput, low-latency multiplayer applications.

    Core components include:

    • Tables: Relational database tables (similar to Postgres) stored in memory and persisted via a Write-Ahead Log (WAL) called the Commitlog.
    • Reducers: Atomic, transactional RPC functions defined in the WASM module that can be called by clients.
    • Subscription Queries: SQL queries made over a WebSocket. They are initially evaluated and then incrementally updated, sending only changes to the client to maintain a low-latency local replica of the server state.
    • Client SDKs: Type-safe libraries code-generated from the tables, types, and reducers defined in the server module.
  2. Use View functions for read-only data derivation

    Views are read-only functions defined with the #[view] macro. They do not modify database state but compute and return results from tables. Views are useful for joining tables, aggregating data, providing user-specific filtered results, and reducing the amount of data sent to clients by computing results server-side. Views can be subscribed to by clients and automatically update when the underlying table data changes.
  3. Authenticate with SpacetimeDB CLI

    Use spacetime login to authenticate via spacetimedb.com. This creates a portable identity that works across different SpacetimeDB servers. If you publish to a server without logging in, the server will issue a local identity specific only to that instance (issuer http://localhost), which cannot be used on other servers.
    spacetime login
  4. Inspect generated Table APIs

    The #[table] macro generates accessor methods, table handles (e.g., table_name__TableHandle), and traits to access those handles via ctx.db. To view the exact generated API for your project:

    1. Run cargo doc --open in the module project directory.
    2. Navigate to the module's documentation to find the generated structs and traits.
  5. Handle errors in SpacetimeDB reducers

    When managing data consistency and errors in reducers, you can choose between returning Result::Err and using panic!. Returning Result::Err is preferred for predictable error conditions because it avoids the performance penalty of recreating the WASM instance. Use panic! only for exceptional, unrecoverable circumstances or unhandled bugs.
  6. Subscribe to SpacetimeDB data in C#

    Clients receive data by subscribing to SQL queries against public tables using the SubscriptionBuilder.

    • OnApplied: Callback triggered when initial data for the subscription arrives.
    • OnError: Callback triggered if the subscription fails (e.g., due to invalid SQL).
    • Subscribe: Accepts an array of SQL query strings and returns a SubscriptionHandle.
    • SubscribeToAllTables(): A convenience method to subscribe to all tables (cannot be easily unsubscribed).
    • Unsubscribing: Use handle.Unsubscribe() or handle.UnsubscribeThen(callback) to stop receiving updates.
    using SpacetimeDB.Types;
    using System.Linq;
    
    private void SubscribeToTables(DbConnection conn)
    {
        conn.SubscriptionBuilder()
            .OnApplied(on_subscription_applied)
            .OnError((errCtx, err) => {
                Console.WriteLine($"Subscription failed: {err.Message}");
            })
            .Subscribe(new string[] { "SELECT * FROM Player", "SELECT * FROM Message" });
    }
    
    private void OnSubscriptionApplied(SubscriptionEventContext ctx)
    {
        // Access initial data via ctx.Db
        var messages = ctx.Db.Message.Iter().ToList();
        messages.Sort((a, b) => a.Sent.CompareTo(b.Sent));
    }
  7. Set up a C# Client SDK project

    To build a native C# client (including Unity), you must install the SDK and generate bindings based on your server module's schema.
    # For .NET Console/Desktop Apps
    dotnet new console -o my_csharp_client
    cd my_csharp_client
    dotnet add package SpacetimeDB.ClientSDK
    
    # Generate module bindings
    mkdir -p module_bindings
    spacetime generate --lang csharp \ 
        --out-dir module_bindings \ 
        --module-path ../path/to/your/server_module
  8. Implement scheduled reducers in Rust

    Scheduled reducers allow you to execute logic at a specific time or periodically (e.g., for game loops). Scheduling is transactional: if a reducer that schedules another reducer fails, the scheduled call will not be created.

    To implement a scheduled reducer:

    1. Define a table with the scheduled(reducer_function_name) parameter in the #[table] attribute.
    2. The table must contain a primary key (typically #[auto_inc]) and a field of type spacetimedb::ScheduleAt.
    3. Define the reducer function with the #[reducer] attribute. Its second argument must be the scheduling table struct.

    Scheduling Types:

    • One-off: Create ScheduleAt from a spacetimedb::Timestamp.
    • Periodic: Create ScheduleAt from a spacetimedb::TimeDuration or std::time::Duration.

    Security Note: Scheduled reducers are still public. To prevent clients from calling them directly, verify that ctx.sender() == ctx.identity().

    use spacetimedb::{table, reducer, ReducerContext, Timestamp, TimeDuration, ScheduleAt, Table};
    use log::info;
    
    #[table(accessor = send_message_schedule, scheduled(send_message))]
    struct SendMessageSchedule {
        #[primary_key]
        #[auto_inc]
        scheduled_id: u64,
        scheduled_at: ScheduleAt,
        text: String,
    }
    
    #[reducer]
    fn send_message(ctx: &ReducerContext, args: SendMessageSchedule) -> Result<(), String> {
        // Security check: ensure only the scheduler (the module itself) can call this
        if ctx.sender() != ctx.identity() {
            return Err("Reducer `send_message` may not be invoked by clients, only via scheduling.".into());
        }
    
        info!("Scheduled SendMessage: {}", args.text);
        Ok(())
    }
    
    #[reducer(init)]
    fn init(ctx: &ReducerContext) -> Result<(), String> {
        let ten_seconds = TimeDuration::from_micros(10_000_000);
        
        // Schedule one-off
        let future_timestamp: Timestamp = ctx.timestamp + ten_seconds;
        ctx.db.send_message_schedule().insert(SendMessageSchedule {
            scheduled_id: 0,
            text: "One-time message".to_string(),
            scheduled_at: future_timestamp.into()
        });
    
        // Schedule periodic (every 10 seconds)
        ctx.db.send_message_schedule().insert(SendMessageSchedule {
            scheduled_id: 0,
            text: "Periodic message".to_string(),
            scheduled_at: ten_seconds.into()
        });
    
        Ok(())
    }
  9. Build a SpacetimeDB client with Rust

    To build a native Rust client, you must first add the spacetimedb_sdk crate and generate module bindings based on your server-side module.

    Setup Steps:

    1. Create a Rust binary project and add spacetimedb_sdk.
    2. Generate bindings using the CLI: spacetime generate --lang rust --out-dir <dir> --module-path <path>.
    3. Declare the generated module in your code (e.g., mod module_bindings;).

    Connection Management: Use DbConnection::builder() to configure the connection:

    • .with_uri(url): The SpacetimeDB instance URI.
    • .with_database_name(name): The target database.
    • .with_token(token): An optional identity token. If omitted, the server generates a new one during on_connect.
    • .on_connect(...): Callback for successful connection; used to save tokens and register table callbacks.
    • .on_connect_error(...) and .on_disconnect(...): Lifecycle error handling.

    Processing Messages: Once connected, you must process the connection loop using one of these methods:

    • connection.run_threaded(): Background thread processing.
    • connection.run_async(): Async runtime integration (e.g., Tokio).
    • connection.frame_tick(): Manual polling (ideal for game loops).
    # Project Setup
    cargo new my_rust_client
    cd my_rust_client
    cargo add spacetimedb_sdk
    
    # Generate Bindings
    mkdir -p src/module_bindings
    spacetime generate --lang rust \
        --out-dir src/module_bindings \
        --module-path ../path/to/your/server_module
    use spacetimedb_sdk::{identity, DbContext, Identity, credentials};
    use crate::module_bindings::{DbConnection, connect_event_callbacks, table_update_callbacks};
    
    const HOST: &str = "http://localhost:3000";
    const DB_NAME: &str = "my_database";
    
    fn connect_to_db() -> DbConnection {
        fn creds_store() -> credentials::File {
            credentials::File::new(".my_client_creds")
        }
    
        DbConnection::builder()
            .with_uri(HOST)
            .with_database_name(DB_NAME)
            .with_token(creds_store().load().ok())
            .on_connect(|conn, identity, auth_token| {
                if let Err(e) = creds_store().save(auth_token) {
                    eprintln!("Failed to save auth token: {}", e);
                }
                connect_event_callbacks(conn);
                table_update_callbacks(conn);
            })
            .on_connect_error(|_, err| {
                eprintln!("Connection Error: {}", err);
                std::process::exit(1);
            })
            .build()
            .expect("Failed to connect")
    }
  10. Publish a module to a SpacetimeDB server

    Deploy a compiled module to a local instance or a managed server (like Maincloud). Publishing creates or updates a database.

    Key Options:

    • [name|identity]: Optional. If provided, the name must match ^[a-z0-9]+(-[a-z0-9]+)*$. If omitted, a unique Identity is assigned automatically.
    • -s, --server <server>: Specify the target (e.g., maincloud.spacetimedb.com). Defaults to local if omitted.
    • --module-path: Builds the module from source before publishing.
    • --bin-path <wasm_file>: Publishes a pre-compiled WASM file.
    • -c, --delete-data: Destroys all existing data when updating an existing database identity.
    # Build and publish from source to 'my-database-name' on the default server
    spacetime publish --module-path my_server_module my-database-name
    
    # Publish a pre-compiled wasm to Maincloud, clearing existing data
    spacetime publish --bin-path ./my_module/target/wasm32-wasi/debug/my_module.wasm -s maincloud -c my-cloud-db-identity
  11. Handle real-time table updates and initial data loads

    SpacetimeDB provides row callbacks (onInsert, onUpdate, onDelete) on table objects.

    Distinguishing Initial Load from Live Updates: The first argument to the callback is ctx: EventContext | undefined.

    • If ctx is defined, the event is a live update.
    • If ctx is undefined, the event is part of the initial subscription load.

    To process the initial cache manually, you can iterate over the table using .iter().

    // Registering callbacks
    this.conn.db.User.onInsert((ctx, user) => {
        if (ctx) {
            console.log(`${user.name} connected (Live Update).`);
        } else {
            console.log(`Loaded user: ${user.name} (Initial Load).`);
        }
    });
    
    // Manually iterating initial cache
    for (const user of this.conn.db.User.iter()) {
        // Process user
    }
  12. Create and manage scheduled reducers in C#

    Scheduled reducers allow executing logic at a specific time or periodically. Scheduling is transactional: if the reducer that schedules another task fails, the scheduled task is not created.

    Implementation Steps

    1. Define a Schedule Table: Create a struct/class with the [Table] attribute. You must specify Scheduled (the name of the reducer method) and ScheduledAt (the name of the field holding the timing info). The table must have a primary key and a field of type SpacetimeDB.ScheduleAt.
    2. Define the Reducer: Create a static reducer method that accepts ReducerContext and an instance of the schedule table struct.
    3. Schedule the Invocation: Insert a row into the schedule table. Set the ScheduleAt field to either:
      • new ScheduleAt.Time(timestamp): For a one-time execution.
      • new ScheduleAt.Interval(timeDuration): For periodic execution.

    Security and Behavior

    • Access Control: Scheduled reducers are still public. To prevent clients from calling them directly, verify that ctx.Sender equals ctx.Identity (the module's own identity).
    • Context: In a scheduled reducer, ctx.Sender is the module's identity and ctx.ConnectionId is null because the call originates from the internal scheduler.
    • Reliability: Execution is best-effort and may be slightly delayed under heavy load.
    using SpacetimeDB;
    using System;
    
    public static partial class Module
    {
        [Table(Accessor = "send_message_schedule", Scheduled = nameof(SendMessage), ScheduledAt = nameof(ScheduledAt))]
        public partial struct SendMessageSchedule
        {
            [PrimaryKey]
            [AutoInc]
            public ulong Id;
            public ScheduleAt ScheduledAt;
            public string Message;
        }
    
        [Reducer]
        public static void SendMessage(ReducerContext ctx, SendMessageSchedule scheduleArgs)
        {
            // Security check: ensure only the scheduler can call this
            if (!ctx.Sender.Equals(ctx.Identity))
            {
                throw new Exception("Reducer SendMessage may not be invoked by clients, only via scheduling.");
            }
            Log.Info($"Scheduled SendMessage: {scheduleArgs.Message}");
        }
    
        [Reducer(ReducerKind.Init)]
        public static void Init(ReducerContext ctx)
        {
            var tenSeconds = new TimeDuration { Microseconds = 10_000_000 };
            var futureTimestamp = ctx.Timestamp + tenSeconds;
    
            // One-off execution
            ctx.Db.send_message_schedule.Insert(new SendMessageSchedule
            {
                Id = 0, 
                ScheduledAt = new ScheduleAt.Time(futureTimestamp),
                Message = "One-time message!"
            });
    
            // Periodic execution
            ctx.Db.send_message_schedule.Insert(new SendMessageSchedule
            {
                Id = 0, 
                ScheduledAt = new ScheduleAt.Interval(tenSeconds),
                Message = "Periodic message every 10s!"
            });
        }
    }