🎯

spacetimedb-concepts

🎯Skill

from douglance/spacetimedb

VibeIndex|
What it does

Explains SpacetimeDB's unique architecture, core concepts, and best practices for building WebAssembly-powered, transactional database applications.

πŸ“¦

Part of

douglance/spacetimedb(5 items)

spacetimedb-concepts

Installation

Install ScriptRun install script
curl -sSf https://install.spacetimedb.com | sh
Install ScriptRun install script
curl https://sh.rustup.rs -sSf | sh
git cloneClone repository
git clone https://github.com/clockworklabs/SpacetimeDB
DockerRun with Docker
docker run --rm --pull always -p 3000:3000 clockworklabs/spacetime start
πŸ“– Extracted from docs: douglance/spacetimedb
3Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

Understand SpacetimeDB architecture and core concepts. Use when learning SpacetimeDB or making architectural decisions.

Overview

# SpacetimeDB Core Concepts

SpacetimeDB is a relational database that is also a server. It lets you upload application logic directly into the database via WebAssembly modules, eliminating the traditional web/game server layer entirely.

---

Critical Rules (Read First)

These five rules prevent the most common SpacetimeDB mistakes:

  1. Reducers are transactional β€” they do not return data to callers. Use subscriptions to read data.
  2. Reducers must be deterministic β€” no filesystem, network, timers, or random. All state must come from tables.
  3. Read data via tables/subscriptions β€” not reducer return values. Clients get data through subscribed queries.
  4. Auto-increment IDs are not sequential β€” gaps are normal, do not use for ordering. Use timestamps or explicit sequence columns.
  5. ctx.sender is the authenticated principal β€” never trust identity passed as arguments. Always use ctx.sender for authorization.

---

Feature Implementation Checklist

When implementing a feature that spans backend and client:

  1. Backend: Define table(s) to store the data
  2. Backend: Define reducer(s) to mutate the data
  3. Client: Subscribe to the table(s)
  4. Client: Call the reducer(s) from UI β€” do not skip this step
  5. Client: Render the data from the table(s)

Common mistake: Building backend tables/reducers but forgetting to wire up the client to call them.

---

Debugging Checklist

When things are not working:

  1. Is SpacetimeDB server running? (spacetime start)
  2. Is the module published? (spacetime publish)
  3. Are client bindings generated? (spacetime generate)
  4. Check server logs for errors (spacetime logs )
  5. Is the reducer actually being called from the client?

---

CLI Commands

```bash

# Start local SpacetimeDB

spacetime start

# Publish module

spacetime publish --project-path

# Clear and republish

spacetime publish --clear-database -y --project-path

# Generate client bindings

spacetime generate --lang --out-dir --project-path

# View logs

spacetime logs

```

---

What SpacetimeDB Is

SpacetimeDB combines a database and application server into a single deployable unit. Clients connect directly to the database and execute application logic inside it. The system is optimized for real-time applications requiring maximum speed and minimum latency.

Key characteristics:

  • In-memory execution: All application state lives in memory for sub-millisecond access
  • Persistent storage: Data is automatically persisted to a write-ahead log (WAL) for durability
  • Real-time synchronization: Changes are automatically pushed to subscribed clients
  • Single deployment: No separate servers, containers, or infrastructure to manage

SpacetimeDB powers BitCraft Online, an MMORPG where the entire game backend (chat, items, resources, terrain, player positions) runs as a single SpacetimeDB module.

The Five Zen Principles

SpacetimeDB is built on five core principles that guide both development and usage:

  1. Everything is a Table: Your entire application state lives in tables. No separate cache layer, no Redis, no in-memory state to synchronize. The database IS your state.
  1. Everything is Persistent: SpacetimeDB persists everything by default, including full history. Persistence only increases latency, never decreases throughput. Modern SSDs can write 15+ GB/s.
  1. Everything is Real-Time: Clients are replicas of server state. Subscribe to data and it flows automatically. No polling, no fetching.
  1. Everything is Transactional: Every reducer runs atomically. Either all changes succeed or all roll back. No partial updates, no corrupted state.
  1. Everything is Programmable: Modules are real code (Rust, C#, TypeScript) running inside the database. Full Turing-complete power for any logic.

Tables

Tables store all data in SpacetimeDB. They use the relational model and support SQL queries for subscriptions.

Defining Tables

Tables are defined using language-specific attributes:

Rust:

```rust

#[spacetimedb::table(name = player, public)]

pub struct Player {

#[primary_key]

#[auto_inc]

id: u32,

#[index(btree)]

name: String,

#[unique]

email: String,

}

```

C#:

```csharp

[SpacetimeDB.Table(Name = "Player", Public = true)]

public partial struct Player

{

[SpacetimeDB.PrimaryKey]

[SpacetimeDB.AutoInc]

public uint Id;

[SpacetimeDB.Index.BTree]

public string Name;

[SpacetimeDB.Unique]

public string Email;

}

```

TypeScript:

```typescript

const players = table(

{ name: 'players', public: true },

{

id: t.u32().primaryKey().autoInc(),

name: t.string().index('btree'),

email: t.string().unique(),

}

);

```

Table Visibility

  • Private tables (default): Only accessible by reducers and the database owner
  • Public tables: Exposed for client read access through subscriptions. Writes still require reducers.

Table Design Principles

Organize data by access pattern, not by entity:

Decomposed approach (recommended):

```

Player PlayerState PlayerStats

id <-- player_id player_id

name position_x total_kills

position_y total_deaths

velocity_x play_time

velocity_y

```

Benefits:

  • Reduced bandwidth (clients subscribing to positions do not receive settings updates)
  • Cache efficiency (similar update frequencies in contiguous memory)
  • Schema evolution (add columns without affecting other tables)
  • Semantic clarity (each table has single responsibility)

Reducers

Reducers are transactional functions that modify database state. They are the ONLY way to mutate tables in SpacetimeDB.

Key Properties

  • Transactional: Run in isolated database transactions
  • Atomic: Either all changes succeed or all roll back
  • Isolated: Cannot interact with the outside world (no network, no filesystem)
  • Callable: Clients invoke reducers as remote procedure calls

Critical Reducer Rules

  1. No global state: Relying on static variables is undefined behavior
  2. No side effects: Reducers cannot make network requests or access files
  3. Store state in tables: All persistent state must be in tables
  4. No return data: Reducers do not return data to callers β€” use subscriptions
  5. Must be deterministic: No random, no timers, no external I/O

Defining Reducers

Rust:

```rust

#[spacetimedb::reducer]

pub fn create_user(ctx: &ReducerContext, name: String, email: String) -> Result<(), String> {

if name.is_empty() {

return Err("Name cannot be empty".to_string());

}

ctx.db.user().insert(User { id: 0, name, email });

Ok(())

}

```

C#:

```csharp

[SpacetimeDB.Reducer]

public static void CreateUser(ReducerContext ctx, string name, string email)

{

if (string.IsNullOrEmpty(name))

throw new ArgumentException("Name cannot be empty");

ctx.Db.User.Insert(new User { Id = 0, Name = name, Email = email });

}

```

ReducerContext

Every reducer receives a ReducerContext providing:

  • ctx.db: Access to all tables (read and write)
  • ctx.sender: The Identity of the caller (use this for authorization, never trust args)
  • ctx.connection_id: The connection ID of the caller
  • ctx.timestamp: The current timestamp

Subscriptions

Subscriptions replicate database rows to clients in real-time. When you subscribe to a query, SpacetimeDB sends matching rows immediately and pushes updates whenever those rows change.

How Subscriptions Work

  1. Subscribe: Register SQL queries describing needed data
  2. Receive initial data: All matching rows are sent immediately
  3. Receive updates: Real-time updates when subscribed rows change
  4. React to changes: Use callbacks (onInsert, onDelete, onUpdate) to handle changes

Client-Side Usage

TypeScript:

```typescript

const conn = DbConnection.builder()

.withUri('wss://maincloud.spacetimedb.com')

.withModuleName('my_module')

.onConnect((ctx) => {

ctx.subscriptionBuilder()

.onApplied(() => console.log('Subscription ready!'))

.subscribe(['SELECT FROM user', 'SELECT FROM message']);

})

.build();

// React to changes

conn.db.user.onInsert((ctx, user) => console.log(New user: ${user.name}));

conn.db.user.onDelete((ctx, user) => console.log(User left: ${user.name}));

conn.db.user.onUpdate((ctx, old, new_) => console.log(${old.name} -> ${new_.name}));

```

Subscription Best Practices

  1. Group subscriptions by lifetime: Keep always-needed data separate from temporary subscriptions
  2. Subscribe before unsubscribing: When updating subscriptions, subscribe to new data first
  3. Avoid overlapping queries: Distinct queries returning overlapping data cause redundant processing
  4. Use indexes: Queries on indexed columns are efficient; full table scans are expensive

Modules

Modules are WebAssembly bundles containing application logic that runs inside the database.

Module Components

  • Tables: Define the data schema
  • Reducers: Define callable functions that modify state
  • Views: Define read-only computed queries
  • Procedures: (Beta) Functions that can have side effects (HTTP requests)

Module Languages

Server-side modules can be written in:

  • Rust
  • C#
  • TypeScript (beta)

Module Lifecycle

  1. Write: Define tables and reducers in your chosen language
  2. Compile: Build to WebAssembly using the SpacetimeDB CLI
  3. Publish: Upload to a SpacetimeDB host with spacetime publish
  4. Hot-swap: Republish to update code without disconnecting clients

Identity

Identity is SpacetimeDB's authentication system based on OpenID Connect (OIDC).

Identity Concepts

  • Identity: A long-lived, globally unique identifier for a user. Derived from OIDC issuer and subject claims.
  • ConnectionId: Identifies a specific client connection. A user may have multiple connections.

Identity in Reducers

```rust

#[spacetimedb::reducer]

pub fn do_something(ctx: &ReducerContext) {

let caller_identity = ctx.sender; // Who is calling this reducer?

// Use identity for authorization checks

// NEVER trust identity passed as a reducer argument

}

```

Authentication Providers

SpacetimeDB works with any OIDC provider:

  • SpacetimeAuth: Built-in managed provider (simple, production-ready)
  • Third-party: Auth0, Clerk, Keycloak, Google, GitHub, etc.

SATS (SpacetimeDB Algebraic Type System)

SATS is the type system and serialization format used throughout SpacetimeDB.

Core Types

| Category | Types |

|----------|-------|

| Primitives | Bool, U8-U256, I8-I256, F32, F64, String |

| Composite | ProductType (structs), SumType (enums/tagged unions) |

| Collections | Array, Map |

| Special | Identity, ConnectionId, ScheduleAt |

Serialization Formats

  • BSATN: Binary format for module-host communication and row storage
  • SATS-JSON: JSON format for HTTP API and WebSocket text protocol

Type Compatibility

Types must implement SpacetimeType to be used in tables and reducers. This is automatic for primitive types and structs using the appropriate attributes.

Client-Server Data Flow

Write Path (Client to Database)

  1. Client calls reducer (e.g., ctx.reducers.createUser("Alice"))
  2. Request sent over WebSocket to SpacetimeDB host
  3. Host validates identity and executes reducer in transaction
  4. On success, changes are committed; on error, all changes roll back
  5. Subscribed clients receive updates for affected rows

Read Path (Database to Client)

  1. Client subscribes with SQL queries (e.g., SELECT * FROM user)
  2. Server evaluates query and sends matching rows
  3. Client maintains local cache of subscribed data
  4. When subscribed data changes, server pushes delta updates
  5. Client cache is automatically updated; callbacks fire

Data Flow Diagram

```

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ CLIENT β”‚

β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚

β”‚ β”‚ Reducers │────>β”‚ Local Cache (Read) β”‚ β”‚

β”‚ β”‚ (Write) β”‚ β”‚ - Tables from subscriptionsβ”‚ β”‚

β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - Automatically synced β”‚ β”‚

β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”‚ WebSocket β”‚ Updates pushed

v β”‚

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚ SpacetimeDB β”‚

β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚

β”‚ β”‚ Module β”‚ β”‚

β”‚ β”‚ - Reducers (transactional logic) β”‚ β”‚

β”‚ β”‚ - Tables (in-memory + persisted) β”‚ β”‚

β”‚ β”‚ - Subscriptions (real-time queries) β”‚ β”‚

β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

```

When to Use SpacetimeDB

Ideal Use Cases

  • Real-time games: MMOs, multiplayer games, turn-based games
  • Collaborative applications: Document editing, whiteboards, design tools
  • Chat and messaging: Real-time communication with presence
  • Live dashboards: Streaming analytics and monitoring
  • IoT applications: Sensor data with real-time updates

Key Decision Factors

Choose SpacetimeDB when you need:

  • Sub-10ms latency for reads and writes
  • Automatic real-time synchronization
  • Transactional guarantees for all operations
  • Simplified architecture (no separate cache, queue, or server)

Less Suitable For

  • Batch analytics: SpacetimeDB is optimized for OLTP, not OLAP
  • Large blob storage: Better suited for structured relational data
  • Stateless APIs: Traditional REST APIs do not need real-time sync

Comparison to Traditional Architectures

Traditional Stack

```

Client

β”‚

v

Load Balancer

β”‚

v

Web/Game Servers (stateless or stateful)

β”‚

β”œβ”€β”€> Cache (Redis)

β”‚

v

Database (PostgreSQL, MySQL)

β”‚

v

Message Queue (for real-time)

```

Pain points:

  • Multiple systems to deploy and manage
  • Cache invalidation complexity
  • State synchronization between servers
  • Manual real-time implementation
  • Horizontal scaling complexity

SpacetimeDB Stack

```

Client

β”‚

v

SpacetimeDB Host

β”‚

v

Module (your logic + tables)

```

Benefits:

  • Single deployment target
  • No cache layer needed (in-memory by design)
  • Automatic real-time synchronization
  • Built-in horizontal scaling (future)
  • Transactional guarantees everywhere

Smart Contract Comparison

SpacetimeDB modules are conceptually similar to smart contracts:

  • Application logic runs inside the data layer
  • Transactions are atomic and verified
  • State changes are deterministic

Key differences:

  • SpacetimeDB is orders of magnitude faster (no consensus overhead)
  • Full relational database capabilities
  • No blockchain or cryptocurrency involved
  • Designed for real-time, not eventual consistency

Common Patterns

Authentication check in reducer:

```rust

#[spacetimedb::reducer]

fn admin_action(ctx: &ReducerContext) -> Result<(), String> {

let admin = ctx.db.admin().identity().find(&ctx.sender)

.ok_or("Not an admin")?;

// ... perform admin action

Ok(())

}

```

Moving between tables (state machine):

```rust

#[spacetimedb::reducer]

fn login(ctx: &ReducerContext) -> Result<(), String> {

let player = ctx.db.logged_out_player().identity().find(&ctx.sender)

.ok_or("Not found")?;

ctx.db.player().insert(player.clone());

ctx.db.logged_out_player().identity().delete(&ctx.sender);

Ok(())

}

```

Scheduled reducer:

```rust

#[spacetimedb::table(name = reminder, scheduled(send_reminder))]

pub struct Reminder {

#[primary_key]

#[auto_inc]

id: u64,

scheduled_at: ScheduleAt,

message: String,

}

#[spacetimedb::reducer]

fn send_reminder(ctx: &ReducerContext, reminder: Reminder) {

// This runs at the scheduled time

log::info!("Reminder: {}", reminder.message);

}

```

---

Editing Behavior

When modifying SpacetimeDB code:

  • Make the smallest change necessary
  • Do NOT touch unrelated files, configs, or dependencies
  • Do NOT invent new SpacetimeDB APIs β€” use only what exists in docs or this repo