spacetimedb-concepts
Understand SpacetimeDB architecture and core concepts. Use when learning SpacetimeDB or making architectural decisions.
How do I install this agent skill?
npx skills add https://github.com/clockworklabs/spacetimedb --skill spacetimedb-conceptsIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill is an educational resource and architectural guide for SpacetimeDB. It contains documentation, code snippets, and standard CLI commands for the platform. No security vulnerabilities or malicious patterns were detected.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
- Runlayerpass
1 file scanned · No issues
- ZeroLeakspass
Score: 93/100 · 2 sections analyzed
What does this agent skill do?
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
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/clockworklabs/spacetimedb/spacetimedb-concepts">View spacetimedb-concepts on skillZs</a>