Back to Spacetimedb

Lifecycle Reducers

docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md

2.10.09.2 KB
Original Source

import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import { CppModuleVersionNotice } from "@site/src/components/CppModuleVersionNotice";

Special reducers handle system events during the database lifecycle.

Init Reducer

Runs once when the module is first published or when the database is cleared.

<Tabs groupId="server-language" queryString> <TabItem value="typescript" label="TypeScript">
typescript
export const init = spacetimedb.init((ctx) => {
  console.log('Database initializing...');
  
  // Set up default data
  if (ctx.db.settings.count() === 0n) {
    ctx.db.settings.insert({
      key: 'welcome_message',
      value: 'Hello, SpacetimeDB!'
    });
  }
});
</TabItem> <TabItem value="csharp" label="C#">
csharp
[SpacetimeDB.Reducer(ReducerKind.Init)]
public static void Init(ReducerContext ctx)
{
    Log.Info("Database initializing...");
    
    // Set up default data
    if (ctx.Db.Settings.Count == 0)
    {
        ctx.Db.Settings.Insert(new Settings
        {
            Key = "welcome_message",
            Value = "Hello, SpacetimeDB!"
        });
    }
}
</TabItem> <TabItem value="rust" label="Rust">
rust
#[reducer(init)]
pub fn init(ctx: &ReducerContext) -> Result<(), String> {
    log::info!("Database initializing...");
    
    // Set up default data
    if ctx.db.settings().count() == 0 {
        ctx.db.settings().try_insert(Settings {
            key: "welcome_message".to_string(),
            value: "Hello, SpacetimeDB!".to_string(),
        })?;
    }
    
    Ok(())
}
</TabItem> <TabItem value="cpp" label="C++"> <CppModuleVersionNotice />
cpp
#include <spacetimedb.h>
using namespace SpacetimeDB;

struct Settings {
    std::string key;
    std::string value;
};
SPACETIMEDB_STRUCT(Settings, key, value);
SPACETIMEDB_TABLE(Settings, settings, Private);
FIELD_Unique(settings, key);

SPACETIMEDB_INIT(init, ReducerContext ctx) {
    LOG_INFO("Database initializing...");
    
    // Set up default data
    if (ctx.db[settings].count() == 0) {
        ctx.db[settings].insert(Settings{
            "welcome_message",
            "Hello, SpacetimeDB!"
        });
    }
    
    return Ok();
}
</TabItem> </Tabs>

The init reducer:

  • Cannot take arguments beyond ReducerContext
  • Runs when publishing with spacetime publish
  • Runs when clearing with spacetime publish -c
  • Failure prevents publishing or clearing

:::tip Module Owner In the init reducer, ctx.sender() is the module owner — the identity of the user who published the database. This is the only place where the owner identity is automatically provided, so if you need to reference it later (e.g. for authorization), store it in a table during init:

<Tabs groupId="server-language" queryString> <TabItem value="typescript" label="TypeScript">
typescript
const config = table({ name: 'config' }, {
  ownerIdentity: t.identity().primaryKey(),
});

export const init = spacetimedb.init((ctx) => {
  ctx.db.config.insert({ ownerIdentity: ctx.sender });
});
</TabItem> <TabItem value="csharp" label="C#">
csharp
[SpacetimeDB.Table(Accessor = "Config")]
public partial struct Config
{
    [SpacetimeDB.PrimaryKey]
    public Identity OwnerIdentity;
}

[SpacetimeDB.Reducer(ReducerKind.Init)]
public static void Init(ReducerContext ctx)
{
    ctx.Db.Config.Insert(new Config { OwnerIdentity = ctx.Sender });
}
</TabItem> <TabItem value="rust" label="Rust">
rust
#[table(accessor = config)]
pub struct Config {
    #[primary_key]
    pub owner_identity: Identity,
}

#[reducer(init)]
pub fn init(ctx: &ReducerContext) -> Result<(), String> {
    ctx.db.config().try_insert(Config {
        owner_identity: ctx.sender(),
    })?;
    Ok(())
}
</TabItem> </Tabs>

You can then check ctx.sender() against the stored owner identity in other reducers to restrict admin-only operations. :::

Client Connected

Runs when a client establishes a connection.

<Tabs groupId="server-language" queryString> <TabItem value="typescript" label="TypeScript">
typescript
export const onConnect = spacetimedb.clientConnected((ctx) => {
  console.log(`Client connected: ${ctx.sender}`);
  
  // ctx.connectionId is guaranteed to be defined
  const connId = ctx.connectionId!;
  
  // Initialize client session
  ctx.db.sessions.insert({
    connection_id: connId,
    identity: ctx.sender,
    connected_at: ctx.timestamp
  });
});
</TabItem> <TabItem value="csharp" label="C#">
csharp
[SpacetimeDB.Reducer(ReducerKind.ClientConnected)]
public static void OnConnect(ReducerContext ctx)
{
    Log.Info($"Client connected: {ctx.Sender}");
    
    // ctx.ConnectionId is guaranteed to be non-null
    var connId = ctx.ConnectionId!.Value;
    
    // Initialize client session
    ctx.Db.Session.Insert(new Session
    {
        ConnectionId = connId,
        Identity = ctx.Sender,
        ConnectedAt = ctx.Timestamp
    });
}
</TabItem> <TabItem value="rust" label="Rust">
rust
#[reducer(client_connected)]
pub fn on_connect(ctx: &ReducerContext) -> Result<(), String> {
    log::info!("Client connected: {}", ctx.sender());
    
    // ctx.connection_id() is guaranteed to be Some(...)
    let conn_id = ctx.connection_id().unwrap();
    
    // Initialize client session
    ctx.db.sessions().try_insert(Session {
        connection_id: conn_id,
        identity: ctx.sender(),
        connected_at: ctx.timestamp,
    })?;
    
    Ok(())
}
</TabItem> <TabItem value="cpp" label="C++">
cpp
#include <spacetimedb.h>
using namespace SpacetimeDB;

struct Session {
    ConnectionId connection_id;
    Identity identity;
    Timestamp connected_at;
};
SPACETIMEDB_STRUCT(Session, connection_id, identity, connected_at);
SPACETIMEDB_TABLE(Session, sessions, Private);
FIELD_PrimaryKey(sessions, connection_id);

SPACETIMEDB_CLIENT_CONNECTED(on_connect, ReducerContext ctx) {
    LOG_INFO("Client connected: " + ctx.sender().to_string());
    
    // ctx.connection_id is guaranteed to be present
    auto conn_id = ctx.connection_id.value();
    
    // Initialize client session
    ctx.db[sessions].insert(Session{
        conn_id,
        ctx.sender(),
        ctx.timestamp
    });
    
    return Ok();
}
</TabItem> </Tabs>

The client_connected reducer:

  • Cannot take arguments beyond ReducerContext
  • ctx.connection_id() is guaranteed to be present
  • Failure disconnects the client
  • Runs for each distinct connection (WebSocket, HTTP call)

Client Disconnected

Runs when a client connection terminates.

<Tabs groupId="server-language" queryString> <TabItem value="typescript" label="TypeScript">
typescript
export const onDisconnect = spacetimedb.clientDisconnected((ctx) => {
  console.log(`Client disconnected: ${ctx.sender}`);
  
  // ctx.connectionId is guaranteed to be defined
  const connId = ctx.connectionId!;
  
  // Clean up client session
  ctx.db.sessions.connection_id.delete(connId);
});
</TabItem> <TabItem value="csharp" label="C#">
csharp
[SpacetimeDB.Reducer(ReducerKind.ClientDisconnected)]
public static void OnDisconnect(ReducerContext ctx)
{
    Log.Info($"Client disconnected: {ctx.Sender}");
    
    // ctx.ConnectionId is guaranteed to be non-null
    var connId = ctx.ConnectionId!.Value;
    
    // Clean up client session
    ctx.Db.Session.ConnectionId.Delete(connId);
}
</TabItem> <TabItem value="rust" label="Rust">
rust
#[reducer(client_disconnected)]
pub fn on_disconnect(ctx: &ReducerContext) -> Result<(), String> {
    log::info!("Client disconnected: {}", ctx.sender());
    
    // ctx.connection_id() is guaranteed to be Some(...)
    let conn_id = ctx.connection_id().unwrap();
    
    // Clean up client session
    ctx.db.sessions().connection_id().delete(&conn_id);
    
    Ok(())
}
</TabItem> <TabItem value="cpp" label="C++">
cpp
#include <spacetimedb.h>
using namespace SpacetimeDB;

struct Session {
    ConnectionId connection_id;
    Identity identity;
    Timestamp connected_at;
};
SPACETIMEDB_STRUCT(Session, connection_id, identity, connected_at);
SPACETIMEDB_TABLE(Session, sessions, Private);
FIELD_PrimaryKey(sessions, connection_id);

SPACETIMEDB_CLIENT_DISCONNECTED(on_disconnect, ReducerContext ctx) {
    LOG_INFO("Client disconnected: " + ctx.sender().to_string());
    
    // ctx.connection_id is guaranteed to be present
    auto conn_id = ctx.connection_id.value();
    
    // Clean up client session
    ctx.db[sessions_connection_id].delete_by_key(conn_id);
    
    return Ok();
}
</TabItem> </Tabs>

The client_disconnected reducer:

  • Cannot take arguments beyond ReducerContext
  • ctx.connection_id() is guaranteed to be present
  • Failure is logged but doesn't prevent disconnection
  • Runs when connection ends (close, timeout, error)

Scheduled Reducers

Reducers can be triggered at specific times using schedule tables. See Schedule Tables for details on:

  • Defining schedule tables
  • Triggering reducers at specific timestamps
  • Running reducers periodically
  • Canceling scheduled executions

:::info Scheduled Reducer Context Scheduled reducer calls originate from SpacetimeDB itself, not from a client. Therefore:

  • ctx.sender() will be the module's own identity
  • The connection ID will be absent (null in TypeScript, null in C#, None in Rust, and std::nullopt in C++) :::