spacetimedb-concepts
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:
- Reducers are transactional — they do not return data to callers. Use subscriptions to read data.
- Reducers must be deterministic — no filesystem, network, timers, or random. All state must come from tables.
- Read data via tables/subscriptions — not reducer return values. Clients get data through subscribed queries.
- Auto-increment IDs are not sequential — gaps are normal, do not use for ordering. Use timestamps or explicit sequence columns.
ctx.sender()is the authenticated principal — never trust identity passed as arguments. Always usectx.sender()for authorization.
Feature Implementation Checklist
When implementing a feature that spans backend and client:
- Backend: Define table(s) to store the data
- Backend: Define reducer(s) to mutate the data
- Client: Subscribe to the table(s)
- Client: Call the reducer(s) from UI — do not skip this step
- 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:
- Is SpacetimeDB server running? (
spacetime start) - Is the module published? (
spacetime publish) - Are client bindings generated? (
spacetime generate) - Check server logs for errors (
spacetime logs <db-name>) - Is the reducer actually being called from the client?
CLI Commands
spacetime start
spacetime publish <db-name> --module-path <module-path>
spacetime publish <db-name> --clear-database -y --module-path <module-path>
spacetime generate --lang <lang> --out-dir <out> --module-path <module-path>
spacetime logs <db-name>
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: Application state is served from memory for very low-latency 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
The Five Zen Principles
- Everything is a Table: Your entire application state lives in tables. No separate cache layer, no Redis, no in-memory state to synchronize.
- Everything is Persistent: SpacetimeDB persists state by default (for example via WAL-backed durability).
- Everything is Real-Time: Clients are replicas of server state. Subscribe to data and it flows automatically.
- Everything is Transactional: Every reducer runs atomically. Either all changes succeed or all roll back.
- Everything is Programmable: Modules are real code (Rust, C#, TypeScript) running inside the database.
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. In 2.0, use accessor (not name) for the API name:
Rust:
#[spacetimedb::table(accessor = player, public)]
pub struct Player {
#[primary_key]
#[auto_inc]
id: u32,
#[index(btree)]
name: String,
#[unique]
email: String,
}
C#:
[SpacetimeDB.Table(Accessor = "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:
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
Benefits: reduced bandwidth, cache efficiency, schema evolution, semantic clarity.
Reducers
Reducers are transactional functions that modify database state. They are the primary client-invoked mutation path; procedures can also mutate tables by running explicit transactions.
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
- No global state: Relying on static variables is undefined behavior
- No side effects: Reducers cannot make network requests or access files
- Store state in tables: All persistent state must be in tables
- No return data: Reducers do not return data to callers — use subscriptions
- Must be deterministic: No random, no timers, no external I/O
Defining Reducers
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#:
[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:
- Database:
ctx.db(Rust field, TS property) /ctx.Db(C# property) - Sender:
ctx.sender()(Rust method) /ctx.Sender(C# property) /ctx.sender(TS property) - Connection ID:
ctx.connection_id()(Rust method) /ctx.ConnectionId(C# property) /ctx.connectionId(TS property) - Timestamp:
ctx.timestamp(Rust field, TS property) /ctx.Timestamp(C# property)
Event Tables (2.0)
Event tables are the preferred way to broadcast reducer-specific data to clients.
#[table(accessor = damage_event, public, event)]
pub struct DamageEvent {
pub target: Identity,
pub amount: u32,
}
#[reducer]
fn deal_damage(ctx: &ReducerContext, target: Identity, amount: u32) {
ctx.db.damage_event().insert(DamageEvent { target, amount });
}
Clients subscribe to event tables and use on_insert callbacks. Event tables must be subscribed explicitly and are excluded from subscribe_to_all_tables().
Subscriptions
Subscriptions replicate database rows to clients in real-time.
How Subscriptions Work
- Subscribe: Register SQL queries describing needed data
- Receive initial data: All matching rows are sent immediately
- Receive updates: Real-time updates when subscribed rows change
- React to changes: Use callbacks (
onInsert,onDelete,onUpdate)
Subscription Best Practices
- Group subscriptions by lifetime: Keep always-needed data separate from temporary subscriptions
- Subscribe before unsubscribing: When updating subscriptions, subscribe to new data first
- Avoid overlapping queries: Distinct queries returning overlapping data cause redundant processing
- 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
- Event Tables: Broadcast reducer-specific data to clients (2.0)
- 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
- Write: Define tables and reducers in your chosen language
- Compile: Build to WebAssembly using the SpacetimeDB CLI
- Publish: Upload to a SpacetimeDB host with
spacetime publish - Hot-swap: Republish to update code without disconnecting clients
Identity
Identity is SpacetimeDB's authentication system based on OpenID Connect (OIDC).
- Identity: A long-lived, globally unique identifier for a user.
- ConnectionId: Identifies a specific client connection.
#[spacetimedb::reducer]
pub fn do_something(ctx: &ReducerContext) {
let caller_identity = ctx.sender(); // Who is calling?
// NEVER trust identity passed as a reducer argument
}
Authentication Providers
SpacetimeDB works with many OIDC providers, including SpacetimeAuth (built-in), Auth0, Clerk, Keycloak, Google, and GitHub.
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
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: 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
Common Patterns
Authentication check in reducer:
#[spacetimedb::reducer]
fn admin_action(ctx: &ReducerContext) -> Result<(), String> {
let admin = ctx.db.admin().identity().find(&ctx.sender())
.ok_or("Not an admin")?;
Ok(())
}
Scheduled reducer:
#[spacetimedb::table(accessor = 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) {
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