| name | cpp-server |
| description | SpacetimeDB C++ server module SDK reference. Use when writing tables, reducers, or module logic in C++. |
| license | Apache-2.0 |
| metadata | {"author":"clockworklabs","version":"2.0","role":"server","language":"cpp","cursor_globs":"**/*.cpp,**/*.h,**/*.hpp","cursor_always_apply":true} |
SpacetimeDB C++ SDK Reference
Imports
#include <spacetimedb.h>
using namespace SpacetimeDB;
Tables
Register structs with macros, then declare as tables:
struct Entity {
uint64_t id;
Identity owner;
std::string name;
bool active;
};
SPACETIMEDB_STRUCT(Entity, id, owner, name, active)
SPACETIMEDB_TABLE(Entity, entity, Public)
FIELD_PrimaryKeyAutoInc(entity, id)
FIELD_Index(entity, name)
Options:
SPACETIMEDB_TABLE(Type, accessor, Public|Private): regular table
SPACETIMEDB_TABLE(Type, accessor, Public|Private, true): event table
Field constraints:
FIELD_PrimaryKey(accessor, field): primary key
FIELD_PrimaryKeyAutoInc(accessor, field): primary key with auto-increment (use 0 on insert)
FIELD_Unique(accessor, field): unique constraint
FIELD_Index(accessor, field): btree index (enables .filter())
Column Types
| C++ type | Notes |
|---|
uint8_t / uint16_t / uint32_t / uint64_t | unsigned integers |
SpacetimeDB::u128 / SpacetimeDB::u256 | large unsigned integers |
int8_t / int16_t / int32_t / int64_t | signed integers |
SpacetimeDB::i128 / SpacetimeDB::i256 | large signed integers |
float / double | floats |
bool | boolean |
std::string | text |
std::vector<T> | list/array |
std::optional<T> | nullable column |
Identity | user identity |
ConnectionId | connection handle |
Timestamp | server timestamp (microseconds since epoch) |
TimeDuration | duration in microseconds |
ScheduleAt | for scheduled tables |
Indexes
FIELD_Index(entity, name)
FIELD_NamedMultiColumnIndex(score, by_player_and_level, player_id, level)
Range queries (requires #include <spacetimedb/range_queries.h>):
ctx.db[user_age].filter(range_inclusive(uint8_t(18), uint8_t(65)));
ctx.db[user_age].filter(range_from(uint8_t(18)));
Reducers
All reducers return ReducerResult. Use Ok() or Err(message):
SPACETIMEDB_REDUCER(create_entity, ReducerContext ctx, std::string name) {
if (name.empty()) {
return Err("Name cannot be empty");
}
ctx.db[entity].insert(Entity{0, ctx.sender(), name, true});
return Ok();
}
DB Operations
ctx.db[entity].insert(Entity{0, owner, "Sample", true});
ctx.db[entity_id].find(entityId);
ctx.db[entity_identity].find(ctx.sender());
ctx.db[entity_name].filter("Alice");
ctx.db[entity];
ctx.db[entity].count();
if (auto e = ctx.db[entity_id].find(entityId)) {
e->name = "New Name";
ctx.db[entity_id].update(*e);
}
ctx.db[entity_id].delete_by_key(entityId);
Note: Bracket notation ctx.db[accessor] is used for all table access. The accessor name comes from SPACETIMEDB_TABLE and FIELD_* macros.
Lifecycle Hooks
SPACETIMEDB_INIT(init, ReducerContext ctx) {
LOG_INFO("Database initializing...");
return Ok();
}
SPACETIMEDB_CLIENT_CONNECTED(on_connect, ReducerContext ctx) {
LOG_INFO("Connected: " + ctx.sender().to_string());
return Ok();
}
SPACETIMEDB_CLIENT_DISCONNECTED(on_disconnect, ReducerContext ctx) {
LOG_INFO("Disconnected: " + ctx.sender().to_string());
return Ok();
}
Authentication & Timestamps
if (row.owner != ctx.sender()) {
return Err("unauthorized");
}
ctx.db[item].insert(Item{0, ctx.sender(), ctx.timestamp});
Timestamp later = ctx.timestamp + TimeDuration::from_seconds(10);
Reducer Context
ReducerContext is the single source of sender identity, deterministic time, and deterministic randomness inside a reducer. Always go through ctx for these. Standard library clocks and random sources are not available in modules.
ctx.db[table]
ctx.sender()
ctx.timestamp
ctx.connection_id
ctx.identity()
ctx.rng()
ctx.sender_auth()
Scheduled Tables
struct Reminder {
uint64_t scheduled_id;
ScheduleAt scheduled_at;
std::string message;
};
SPACETIMEDB_STRUCT(Reminder, scheduled_id, scheduled_at, message)
SPACETIMEDB_TABLE(Reminder, reminder, Public)
FIELD_PrimaryKeyAutoInc(reminder, scheduled_id)
SPACETIMEDB_SCHEDULE(reminder, 1, send_reminder)
SPACETIMEDB_REDUCER(send_reminder, ReducerContext ctx, Reminder arg) {
LOG_INFO("Reminder: " + arg.message);
return Ok();
}
ctx.db[reminder].insert(Reminder{0, ScheduleAt::time(ctx.timestamp + TimeDuration::from_seconds(10)), "msg"});
ctx.db[reminder].insert(Reminder{0, ScheduleAt::interval(TimeDuration::from_seconds(5)), "msg"});
Custom Types
struct Point { float x; float y; };
SPACETIMEDB_STRUCT(Point, x, y)
SPACETIMEDB_UNIT_TYPE(Active)
SPACETIMEDB_UNIT_TYPE(Inactive)
SPACETIMEDB_ENUM(PlayerStatus,
(Active, Active),
(Inactive, Inactive),
(Suspended, std::string)
)
Logging
LOG_INFO("Message: " + msg);
LOG_WARN("Warning: " + msg);
LOG_ERROR("Error: " + msg);
LOG_DEBUG("Debug: " + msg);
LOG_PANIC("Fatal: " + msg);
Complete Example
#include <spacetimedb.h>
using namespace SpacetimeDB;
struct Entity {
Identity identity;
std::string name;
bool active;
};
SPACETIMEDB_STRUCT(Entity, identity, name, active)
SPACETIMEDB_TABLE(Entity, entity, Public)
FIELD_PrimaryKey(entity, identity)
struct Record {
uint64_t id;
Identity owner;
uint32_t value;
Timestamp created_at;
};
SPACETIMEDB_STRUCT(Record, id, owner, value, created_at)
SPACETIMEDB_TABLE(Record, record, Public)
FIELD_PrimaryKeyAutoInc(record, id)
SPACETIMEDB_CLIENT_CONNECTED(on_connect, ReducerContext ctx) {
if (auto existing = ctx.db[entity_identity].find(ctx.sender())) {
existing->active = true;
ctx.db[entity_identity].update(*existing);
}
return Ok();
}
SPACETIMEDB_CLIENT_DISCONNECTED(on_disconnect, ReducerContext ctx) {
if (auto existing = ctx.db[entity_identity].find(ctx.sender())) {
existing->active = false;
ctx.db[entity_identity].update(*existing);
}
return Ok();
}
SPACETIMEDB_REDUCER(create_entity, ReducerContext ctx, std::string name) {
if (ctx.db[entity_identity].find(ctx.sender())) {
return Err("already exists");
}
ctx.db[entity].insert(Entity{ctx.sender(), name, true});
return Ok();
}
SPACETIMEDB_REDUCER(add_record, ReducerContext ctx, uint32_t value) {
if (!ctx.db[entity_identity].find(ctx.sender())) {
return Err("not found");
}
ctx.db[record].insert(Record{0, ctx.sender(), value, ctx.timestamp});
return Ok();
}