Reducers
Reducers are functions that modify database state in response to client requests or system events. They are the only way to mutate tables in SpacetimeDB - all database changes must go through reducers.
Defining Reducers
Reducers are defined in your module code and automatically exposed as callable functions to connected clients.
- Rust
- C#
- TypeScript
Use the #[spacetimedb::reducer] macro on a function:
use spacetimedb::{reducer, ReducerContext, Table};
#[reducer]
pub fn create_user(ctx: &ReducerContext, name: String, email: String) -> Result<(), String> {
// Validate input
if name.is_empty() {
return Err("Name cannot be empty".to_string());
}
// Modify tables
ctx.db.user().insert(User {
id: 0, // auto-increment will assign
name,
email,
});
Ok(())
}Reducers must take &ReducerContext as their first parameter. Additional parameters must be serializable types. Reducers can return (), Result<(), String>, or Result<(), E> where E: Display.
Use the [SpacetimeDB.Reducer] attribute on a static method:
using SpacetimeDB;
public static partial class Module
{
[SpacetimeDB.Reducer]
public static void CreateUser(ReducerContext ctx, string name, string email)
{
// Validate input
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("Name cannot be empty");
}
// Modify tables
ctx.Db.User.Insert(new User
{
Id = 0, // auto-increment will assign
Name = name,
Email = email
});
}
}Reducers must be static methods with ReducerContext as the first parameter. Additional parameters must be types marked with [SpacetimeDB.Type]. Reducers should return void.
Use the spacetimedb.reducer function:
import { schema, table, t } from 'spacetimedb/server';
spacetimedb.reducer('create_user', { name: t.string(), email: t.string() }, (ctx, { name, email }) => {
// Validate input
if (name === '') {
throw new Error('Name cannot be empty');
}
// Modify tables
ctx.db.user.insert({
id: 0, // auto-increment will assign
name,
email
});
});The first argument is the reducer name, the second defines argument types, and the third is the handler function taking (ctx, args).
Transactional Execution
Every reducer runs inside a database transaction. This provides important guarantees:
- Isolation: Reducers don't see changes from other concurrent reducers
- Atomicity: Either all changes succeed or all are rolled back
- Consistency: Failed reducers leave the database unchanged
If a reducer throws an exception or returns an error, all of its changes are automatically rolled back.
Accessing Tables
Reducers can query and modify tables through the ReducerContext:
- Inserting rows
- Updating rows by unique columns
- Deleting rows
- Querying with indexes
- Iterating all rows
- Counting rows
Reducer Isolation
Reducers run in an isolated environment and cannot interact with the outside world:
- ❌ No network requests
- ❌ No file system access
- ❌ No system calls
- ✅ Only database operations
If you need to interact with external systems, use Procedures instead. Procedures can make network calls and perform other side effects, but they have different execution semantics and limitations.
Next Steps
- Learn about Tables to understand data storage
- Explore Procedures for side effects beyond the database
- Review Subscriptions for real-time client updates