Skip to main content

Overview

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.

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 have full read-write access to all tables (both public and private) through the ReducerContext. The examples below assume a user table with id (primary key), name (indexed), and email (unique) columns.

Inserting Rows

ctx.db.user.insert({
  id: 0,  // auto-increment will assign
  name: 'Alice',
  email: 'alice@example.com'
});

Finding Rows by Unique Column

Use find on a unique or primary key column to retrieve a single row:

const user = ctx.db.user.id.find(123);
if (user) {
  console.log(`Found: ${user.name}`);
}

const byEmail = ctx.db.user.email.find('alice@example.com');

Filtering Rows by Indexed Column

Use filter on an indexed column to retrieve multiple matching rows:

for (const user of ctx.db.user.name.filter('Alice')) {
  console.log(`User ${user.id}: ${user.email}`);
}

Updating Rows

Find a row, modify it, then call update on the same unique column:

const user = ctx.db.user.id.find(123);
if (user) {
  user.name = 'Bob';
  ctx.db.user.id.update(user);
}

Deleting Rows

Delete by unique column value or by indexed column value:

// Delete by primary key
ctx.db.user.id.delete(123);

// Delete all matching an indexed column
const deleted = ctx.db.user.name.delete('Alice');
console.log(`Deleted ${deleted} row(s)`);

Iterating All Rows

Use iter to iterate over all rows in a table:

for (const user of ctx.db.user.iter()) {
  console.log(`${user.id}: ${user.name}`);
}

Counting Rows

Use count to get the number of rows in a table:

const total = ctx.db.user.count();
console.log(`Total users: ${total}`);

For more details on querying with indexes, including range queries and multi-column indexes, see Indexes.

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.

Global and Static Variables Are Undefined Behavior

Relying on global variables, static variables, or module-level state to persist across reducer calls is undefined behavior. SpacetimeDB does not guarantee that values stored in these locations will be available in subsequent reducer invocations.

This is undefined for several reasons:

  1. Fresh execution environments. SpacetimeDB may run each reducer in a fresh WASM or JS instance.
  2. Module updates. Publishing a new module creates a fresh execution environment. This is necessary for hot-swapping modules while transactions are in flight.
  3. Concurrent execution. SpacetimeDB reserves the right to execute multiple reducers concurrently in separate execution environments (e.g., with MVCC).
  4. Crash recovery. Instance memory is not persisted across restarts.
  5. Non-transactional updates. If you modify global state and then roll back the transaction, the modified value may remain for subsequent transactions.
  6. Replay safety. If a serializability anomaly is detected, SpacetimeDB may re-execute your reducer with the same arguments, causing modifications to global state to occur multiple times.

Reducers are designed to be free of side effects. They should only modify tables. Always store state in tables to ensure correctness and durability.

// ❌ Undefined behavior: may or may not persist or correctly update across reducer calls
static mut COUNTER: u64 = 0;

// ✅ Store state in a table instead
#[spacetimedb::table(name = counter)]
pub struct Counter {
    #[primary_key]
    id: u32,
    value: u64,
}

Scheduling Procedures

Reducers cannot call procedures directly (procedures may have side effects incompatible with transactional execution). Instead, schedule a procedure to run by inserting into a schedule table:

import { schema, t, table, SenderError } from 'spacetimedb/server';

// Define a schedule table for the procedure
const fetchSchedule = table(
  { name: 'fetch_schedule', scheduled: 'fetch_external_data' },
  {
    scheduled_id: t.u64().primaryKey().autoInc(),
    scheduled_at: t.scheduleAt(),
    url: t.string(),
  }
);

const spacetimedb = schema(fetchSchedule);

// The procedure to be scheduled
const fetchExternalData = spacetimedb.procedure(
  'fetch_external_data',
  { arg: fetchSchedule.rowType },
  t.unit(),
  (ctx, { arg }) => {
    const response = ctx.http.fetch(arg.url);
    // Process response...
    return {};
  }
);

// From a reducer, schedule the procedure by inserting into the schedule table
const queueFetch = spacetimedb.reducer('queue_fetch', { url: t.string() }, (ctx, { url }) => {
  ctx.db.fetchSchedule.insert({
    scheduled_id: 0n,
    scheduled_at: ScheduleAt.interval(0n), // Run immediately
    url,
  });
});

See Schedule Tables for more scheduling options.

Next Steps

  • Learn about Tables to understand data storage
  • Explore Procedures for side effects beyond the database
  • Review Subscriptions for real-time client updates