diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md
index 3296c4bb96b..1733f8997b4 100644
--- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md
+++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md
@@ -108,6 +108,31 @@ use spacetimedb::Table;
If you see errors like "no method named `try_insert` found", add this import.
:::
+
+
+
+Use the `SPACETIMEDB_REDUCER` macro on a function:
+
+```cpp
+#include
+using namespace SpacetimeDB;
+
+SPACETIMEDB_REDUCER(create_user, ReducerContext ctx, std::string name, std::string email) {
+ // Validate input
+ if (name.empty()) {
+ return Err("Name cannot be empty");
+ }
+
+ // Modify tables
+ User user{0, name, email}; // 0 for id - auto-increment will assign
+ ctx.db[user].insert(user);
+
+ return Ok();
+}
+```
+
+Reducers must take `ReducerContext ctx` as their first parameter. Additional parameters can be any registered types. Reducers return `ReducerResult` (which is `Outcome`): use `Ok()` on success or `Err(message)` on error for convenience.
+
@@ -161,6 +186,17 @@ ctx.db.user().insert(User {
});
```
+
+
+
+```cpp
+ctx.db[user].insert(User{
+ 0, // auto-increment will assign
+ "Alice",
+ "alice@example.com"
+});
+```
+
@@ -204,6 +240,17 @@ if let Some(user) = ctx.db.user().id().find(123) {
let by_email = ctx.db.user().email().find("alice@example.com");
```
+
+
+
+```cpp
+if (auto user = ctx.db[user_id].find(123)) {
+ LOG_INFO("Found: " + user->name);
+}
+
+auto by_email = ctx.db[user_email].find("alice@example.com");
+```
+
@@ -239,6 +286,15 @@ for user in ctx.db.user().name().filter("Alice") {
}
```
+
+
+
+```cpp
+for (const auto& user : ctx.db[user_name].filter("Alice")) {
+ LOG_INFO("User " + std::to_string(user.id) + ": " + user.email);
+}
+```
+
@@ -279,6 +335,16 @@ if let Some(mut user) = ctx.db.user().id().find(123) {
}
```
+
+
+
+```cpp
+if (auto user = ctx.db[user_id].find(123)) {
+ user->name = "Bob";
+ ctx.db[user_id].update(*user);
+}
+```
+
@@ -322,6 +388,22 @@ let deleted = ctx.db.user().name().delete("Alice");
log::info!("Deleted {} row(s)", deleted);
```
+
+
+
+```cpp
+// Delete by primary key
+ctx.db[user_id].delete_by_key(123);
+
+// Delete all matching an indexed column
+uint32_t deleted = 0;
+for (const auto& user : ctx.db[user_name].filter("Alice")) {
+ ctx.db[user_id].delete_by_key(user.id);
+ deleted++;
+}
+LOG_INFO("Deleted " + std::to_string(deleted) + " row(s)");
+```
+
@@ -357,6 +439,15 @@ for user in ctx.db.user().iter() {
}
```
+
+
+
+```cpp
+for (const auto& user : ctx.db[user]) {
+ LOG_INFO(std::to_string(user.id) + ": " + user.name);
+}
+```
+
@@ -388,6 +479,14 @@ let total = ctx.db.user().count();
log::info!("Total users: {}", total);
```
+
+
+
+```cpp
+auto total = ctx.db[user].count();
+LOG_INFO("Total users: " + std::to_string(total));
+```
+
@@ -553,6 +652,48 @@ fn queue_fetch(ctx: &ReducerContext, url: String) {
}
```
+
+
+
+```cpp
+#define SPACETIMEDB_UNSTABLE_FEATURES
+#include
+using namespace SpacetimeDB;
+
+// Define a table to store scheduled tasks
+struct FetchSchedule {
+ uint64_t scheduled_id;
+ ScheduleAt scheduled_at;
+ std::string url;
+};
+SPACETIMEDB_STRUCT(FetchSchedule, scheduled_id, scheduled_at, url);
+SPACETIMEDB_TABLE(FetchSchedule, fetch_schedule, Private);
+FIELD_PrimaryKeyAutoInc(fetch_schedule, scheduled_id);
+
+// Register the table for scheduling (column 1 = scheduled_at field, 0-based index)
+SPACETIMEDB_SCHEDULE(fetch_schedule, 1, fetch_external_data);
+
+// The procedure to be scheduled - called automatically when the time arrives
+SPACETIMEDB_PROCEDURE(uint32_t, fetch_external_data, ProcedureContext ctx, FetchSchedule schedule) {
+ LOG_INFO("Fetching data from: " + schedule.url);
+ // Process response...
+ return 0; // Success
+}
+
+// From a reducer, schedule the procedure by inserting into the schedule table
+SPACETIMEDB_REDUCER(queue_fetch, ReducerContext ctx, std::string url) {
+ auto scheduled_at = ScheduleAt(TimeDuration::from_seconds(0)); // Run immediately
+ FetchSchedule fetch_task{
+ 0, // scheduled_id - auto-increment will assign
+ scheduled_at, // When to execute
+ url
+ };
+ ctx.db[fetch_schedule].insert(fetch_task);
+ LOG_INFO("Fetch scheduled for URL: " + url);
+ return Ok();
+}
+```
+
diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md
index 38f447125d4..98dc5f91468 100644
--- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md
+++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md
@@ -81,6 +81,27 @@ fn create_user(ctx: &ReducerContext, name: String) {
}
```
+
+
+
+```cpp
+#include
+using namespace SpacetimeDB;
+
+struct User {
+ uint64_t id;
+ std::string name;
+};
+SPACETIMEDB_STRUCT(User, id, name);
+SPACETIMEDB_TABLE(User, user, Public);
+FIELD_PrimaryKeyAutoInc(user, id);
+
+SPACETIMEDB_REDUCER(create_user, ReducerContext ctx, std::string name) {
+ ctx.db[user].insert(User{0, name});
+ return Ok();
+}
+```
+
@@ -184,6 +205,35 @@ fn update_score(ctx: &ReducerContext, new_score: u32) {
}
```
+
+
+
+```cpp
+#include
+using namespace SpacetimeDB;
+
+struct Player {
+ Identity identity;
+ std::string name;
+ uint32_t score;
+};
+SPACETIMEDB_STRUCT(Player, identity, name, score);
+SPACETIMEDB_TABLE(Player, player, Public);
+FIELD_PrimaryKey(player, identity);
+
+SPACETIMEDB_REDUCER(update_score, ReducerContext ctx, uint32_t new_score) {
+ // Get the caller's identity
+ auto caller = ctx.sender;
+
+ // Find and update their player record
+ if (auto player = ctx.db[player_identity].find(caller)) {
+ player->score = new_score;
+ ctx.db[player_identity].update(*player);
+ }
+ return Ok();
+}
+```
+
@@ -298,6 +348,36 @@ fn send_reminder(ctx: &ReducerContext, task: ScheduledTask) {
}
```
+
+
+
+```cpp
+#include
+using namespace SpacetimeDB;
+
+struct ScheduledTask {
+ uint64_t task_id;
+ ScheduleAt scheduled_at;
+ std::string message;
+};
+SPACETIMEDB_STRUCT(ScheduledTask, task_id, scheduled_at, message);
+SPACETIMEDB_TABLE(ScheduledTask, scheduled_task, Private);
+FIELD_PrimaryKeyAutoInc(scheduled_task, task_id);
+
+// Register the table for scheduling (column 1 = scheduled_at field, 0-based index)
+SPACETIMEDB_SCHEDULE(scheduled_task, 1, send_reminder);
+
+SPACETIMEDB_REDUCER(send_reminder, ReducerContext ctx, ScheduledTask task) {
+ // Only allow the scheduler (module identity) to call this
+ if (ctx.sender != ctx.identity()) {
+ return Err("This reducer can only be called by the scheduler");
+ }
+
+ LOG_INFO("Reminder: " + task.message);
+ return Ok();
+}
+```
+
@@ -346,4 +426,23 @@ TypeScript uses `Math.random()` for random number generation, which is automatic
- `random() -> T` - Generate a single random value
- `sender_auth() -> &AuthCtx` - Get authorization context for the caller (includes JWT claims and internal call detection)
+
+
+| Property | Type | Description |
+| --------------- | ------------------------------ | ----------------------------------------------- |
+| `db[table]` | `Table` | Access to a specific table's operations |
+| `sender` | `Identity` | Identity of the caller |
+| `timestamp` | `Timestamp` | Time when the reducer was invoked |
+| `connection_id` | `std::optional` | Connection ID of the caller, if available |
+
+**Methods:**
+
+- `identity() -> Identity` - Get the module's identity
+- `rng() -> StdbRng&` - Get the random number generator (deterministic and reproducible)
+- `sender_auth() -> const AuthCtx&` - Get authorization context for the caller (includes JWT claims and internal call detection)
+
+:::note
+C++ uses the `std::optional` type for the `connection_id` to represent values that may not be present. The `rng()` method returns a deterministic random number generator that is seeded consistently across all nodes.
+:::
+
diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md
index 22afb7de483..7bb2f3b1af1 100644
--- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md
+++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md
@@ -71,6 +71,36 @@ pub fn init(ctx: &ReducerContext) -> Result<(), String> {
}
```
+
+
+
+```cpp
+#include
+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();
+}
+```
+
@@ -147,6 +177,39 @@ pub fn on_connect(ctx: &ReducerContext) -> Result<(), String> {
}
```
+
+
+
+```cpp
+#include
+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();
+}
+```
+
@@ -210,6 +273,35 @@ pub fn on_disconnect(ctx: &ReducerContext) -> Result<(), String> {
}
```
+
+
+
+```cpp
+#include
+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();
+}
+```
+
diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00600-error-handling.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00600-error-handling.md
index 714f68d1110..a50e26895ec 100644
--- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00600-error-handling.md
+++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00600-error-handling.md
@@ -101,6 +101,36 @@ pub fn transfer_credits(
```
+
+
+ ```cpp
+ #include
+ using namespace SpacetimeDB;
+
+ struct User {
+ Identity identity;
+ uint32_t credits;
+ };
+ SPACETIMEDB_STRUCT(User, identity, credits);
+ SPACETIMEDB_TABLE(User, users, Private);
+ FIELD_PrimaryKey(users, identity);
+
+ SPACETIMEDB_REDUCER(transfer_credits, ReducerContext ctx, Identity to_user, uint32_t amount) {
+ auto from_user = ctx.db[users_identity].find(ctx.sender);
+ if (!from_user) {
+ return Err("User not found");
+ }
+
+ if (from_user->credits < amount) {
+ return Err("Insufficient credits");
+ }
+
+ // ... perform transfer
+ return Ok();
+ }
+ ```
+
+
### Programmer Errors
@@ -165,6 +195,28 @@ pub fn process_data(ctx: &ReducerContext, data: Vec) -> Result<(), String> {
```
+
+
+ ```cpp
+ #include
+ #include
+ using namespace SpacetimeDB;
+
+ SPACETIMEDB_REDUCER(process_data, ReducerContext ctx, Vec data) {
+ // This indicates a bug
+ assert(!data.empty() && "Unexpected empty data");
+
+ auto parsed = parse_data(data);
+ if (!parsed) {
+ LOG_PANIC("Failed to parse data");
+ }
+
+ // ...
+ return Ok();
+ }
+ ```
+
+
Programmer errors are logged and visible in your project dashboard. Consider setting up alerting to be notified when these occur.
diff --git a/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md b/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md
index 51af1e5a736..c31276de352 100644
--- a/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md
+++ b/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md
@@ -96,6 +96,27 @@ fn add_two_numbers(ctx: &mut spacetimedb::ProcedureContext, lhs: u32, rhs: u32)
}
```
+
+
+
+:::warning Unstable Feature
+Procedures in C++ are currently unstable. To use them, add `#define SPACETIMEDB_UNSTABLE_FEATURES` before including the SpacetimeDB header.
+:::
+
+Define a procedure using the `SPACETIMEDB_PROCEDURE` macro.
+
+The macro's first parameter is the return type, followed by the procedure name. The function's first argument must be of type `ProcedureContext`. By convention, this argument is named `ctx`. A procedure may accept any number of additional arguments and must return a value.
+
+```cpp
+#define SPACETIMEDB_UNSTABLE_FEATURES
+#include
+using namespace SpacetimeDB;
+
+SPACETIMEDB_PROCEDURE(uint64_t, add_two_numbers, ProcedureContext ctx, uint32_t lhs, uint32_t rhs) {
+ return static_cast(lhs) + static_cast(rhs);
+}
+```
+
@@ -244,6 +265,55 @@ values observed during prior runs must not influence the behavior of the functio
Avoid capturing mutable state within functions passed to `with_tx`.
:::
+
+
+
+Unlike reducers, procedures don't automatically run in database transactions.
+This means there's no `ctx.db` field to access the database.
+Instead, procedure code must manage transactions explicitly with `ctx.with_tx`.
+
+```cpp
+#define SPACETIMEDB_UNSTABLE_FEATURES
+#include
+using namespace SpacetimeDB;
+
+struct MyTable {
+ uint32_t a;
+ std::string b;
+};
+SPACETIMEDB_STRUCT(MyTable, a, b)
+SPACETIMEDB_TABLE(MyTable, my_table, Public)
+
+SPACETIMEDB_PROCEDURE(Unit, insert_a_value, ProcedureContext ctx, uint32_t a, std::string b) {
+ ctx.with_tx([&](TxContext& tx) {
+ tx.db[my_table].insert(MyTable{a, b});
+ });
+ return Unit{};
+}
+```
+
+`ctx.with_tx` takes a lambda function with signature `[](TxContext& tx) -> T`.
+Within that function, the `TxContext` can be used to access the database
+[in all the same ways as a `ReducerContext`](/functions/reducers/reducer-context).
+When the function returns, the transaction will be committed,
+and its changes to the database state will become permanent and be broadcast to clients.
+If the function throws an exception, the transaction will be rolled back, and its changes will be discarded.
+However, for transactions that may fail,
+[prefer calling `try_with_tx` and returning `bool`](#fallible-database-operations) rather than throwing.
+
+:::warning
+The function passed to `ctx.with_tx` may be invoked multiple times,
+possibly seeing a different version of the database state each time.
+
+If invoked more than once with reference to the same database state,
+it must perform the same operations and return the same result each time.
+
+If invoked more than once with reference to different database states,
+values observed during prior runs must not influence the behavior of the function or the calling procedure.
+
+Avoid capturing mutable state within functions passed to `with_tx`.
+:::
+
@@ -315,6 +385,36 @@ If the function returns `Ok`, the transaction will be committed,
and its changes to the database state will become permanent and be broadcast to clients.
If that function returns `Err`, the transaction will be rolled back, and its changes will be discarded.
+
+
+
+For fallible database operations, use `ctx.try_with_tx` with a lambda that returns `bool`:
+
+```cpp
+#define SPACETIMEDB_UNSTABLE_FEATURES
+#include
+using namespace SpacetimeDB;
+
+SPACETIMEDB_PROCEDURE(bool, maybe_insert_a_value, ProcedureContext ctx, uint32_t a, std::string b) {
+ return ctx.try_with_tx([&](TxContext& tx) -> bool {
+ if (a < 10) {
+ return false; // Rollback transaction
+ }
+ tx.db[my_table].insert(MyTable{a, b});
+ return true; // Commit transaction
+ });
+}
+```
+
+`ctx.try_with_tx` takes a lambda function with signature `[](TxContext& tx) -> bool`.
+If the function returns `true`, the transaction will be committed,
+and its changes to the database state will become permanent and be broadcast to clients.
+If the function returns `false`, the transaction will be rolled back, and its changes will be discarded.
+
+:::note
+For non-bool return types, `try_with_tx` always commits the transaction. To abort in those cases, use `LOG_PANIC`.
+:::
+
@@ -434,6 +534,47 @@ fn find_highest_level_player(ctx: &mut ProcedureContext) {
}
```
+
+
+
+Functions passed to
+[`ctx.with_tx`](#accessing-the-database) and [`ctx.try_with_tx`](#fallible-database-operations)
+may return a value, and that value will be returned to the calling procedure.
+
+Transaction return values are never saved or broadcast to clients, and are used only by the calling procedure.
+
+```cpp
+#define SPACETIMEDB_UNSTABLE_FEATURES
+#include
+using namespace SpacetimeDB;
+
+struct Player {
+ Identity id;
+ uint32_t level;
+};
+SPACETIMEDB_STRUCT(Player, id, level)
+SPACETIMEDB_TABLE(Player, player, Public)
+
+SPACETIMEDB_PROCEDURE(Unit, find_highest_level_player, ProcedureContext ctx) {
+ auto highest_level_player = ctx.with_tx([](TxContext& tx) -> std::optional {
+ std::optional highest;
+ for (const auto& player : tx.db[player]) {
+ if (!highest || player.level > highest->level) {
+ highest = player;
+ }
+ }
+ return highest;
+ });
+
+ if (highest_level_player) {
+ LOG_INFO("Congratulations to " + highest_level_player->id.to_hex_string());
+ } else {
+ LOG_WARN("No players...");
+ }
+ return Unit{};
+}
+```
+
@@ -639,6 +780,103 @@ fn get_request_with_short_timeout(ctx: &mut spacetimedb::ProcedureContext) {
Procedures can't send requests at the same time as holding open a [transaction](#accessing-the-database).
+
+
+
+:::warning Unstable Feature
+HTTP requests in C++ procedures are currently unstable. To use them, add `#define SPACETIMEDB_UNSTABLE_FEATURES` before including the SpacetimeDB header.
+:::
+
+Procedures can make HTTP requests to external services using methods on `ctx.http`.
+
+`ctx.http.get` performs simple `GET` requests:
+
+```cpp
+#define SPACETIMEDB_UNSTABLE_FEATURES
+#include
+using namespace SpacetimeDB;
+
+SPACETIMEDB_PROCEDURE(Unit, get_request, ProcedureContext ctx) {
+ auto result = ctx.http.get("https://example.invalid");
+
+ if (result.is_ok()) {
+ auto& response = result.value();
+ auto body = response.body.to_string_utf8_lossy();
+ LOG_INFO("Got response with status " + std::to_string(response.status_code) +
+ " and body " + body);
+ } else {
+ LOG_ERROR("Request failed: " + result.error());
+ }
+
+ return Unit{};
+}
+```
+
+`ctx.http.send` sends an `HttpRequest` with custom method, headers, and body:
+
+```cpp
+#define SPACETIMEDB_UNSTABLE_FEATURES
+#include
+using namespace SpacetimeDB;
+
+SPACETIMEDB_PROCEDURE(Unit, post_request, ProcedureContext ctx) {
+ HttpRequest request{
+ .uri = "https://example.invalid/upload",
+ .method = HttpMethod::post(),
+ .headers = {HttpHeader{"Content-Type", "text/plain"}},
+ .body = HttpBody::from_string("This is the body of the HTTP request")
+ };
+
+ auto result = ctx.http.send(request);
+
+ if (result.is_ok()) {
+ auto& response = result.value();
+ auto body = response.body.to_string_utf8_lossy();
+ LOG_INFO("Got response with status " + std::to_string(response.status_code) +
+ " and body " + body);
+ } else {
+ LOG_ERROR("Request failed: " + result.error());
+ }
+
+ return Unit{};
+}
+```
+
+Set a timeout for a request using `TimeDuration::from_millis()`:
+
+```cpp
+#define SPACETIMEDB_UNSTABLE_FEATURES
+#include
+using namespace SpacetimeDB;
+
+SPACETIMEDB_PROCEDURE(Unit, get_request_with_short_timeout, ProcedureContext ctx) {
+ HttpRequest request{
+ .uri = "https://example.invalid",
+ .method = HttpMethod::get(),
+ .timeout = TimeDuration::from_millis(10)
+ };
+
+ auto result = ctx.http.send(request);
+
+ if (result.is_ok()) {
+ auto& response = result.value();
+ auto body = response.body.to_string_utf8_lossy();
+ LOG_INFO("Got response with status " + std::to_string(response.status_code) +
+ " and body " + body);
+ } else {
+ LOG_ERROR("Request failed: " + result.error());
+ }
+
+ return Unit{};
+}
+```
+
+:::note
+All timeouts are clamped to a maximum of 500ms by the host.
+:::
+
+Procedures can't send requests at the same time as holding open a [transaction](#accessing-the-database).
+
@@ -736,6 +974,60 @@ fn fetch_and_process(ctx: &mut ProcedureContext, url: String) -> Result<(), Stri
}
```
+
+
+
+:::warning Unstable Feature
+Procedures in C++ are currently unstable. To use them, add `#define SPACETIMEDB_UNSTABLE_FEATURES` before including the SpacetimeDB header.
+:::
+
+In C++, `TxContext` and `ReducerContext` share the same database API, so it’s common to move shared logic into a helper that takes a `DatabaseContext&` and call it from both the reducer and the procedure.
+
+```cpp
+#define SPACETIMEDB_UNSTABLE_FEATURES
+#include
+using namespace SpacetimeDB;
+
+struct ProcessedItem {
+ uint64_t id;
+};
+SPACETIMEDB_STRUCT(ProcessedItem, id)
+SPACETIMEDB_TABLE(ProcessedItem, processed_item_proc, Public)
+FIELD_PrimaryKey(processed_item_proc, id)
+
+static void process_item_logic(DatabaseContext& db, uint64_t item_id) {
+ db[processed_item_proc].insert(ProcessedItem{item_id});
+}
+
+SPACETIMEDB_REDUCER(process_item, ReducerContext& ctx, uint64_t item_id) {
+ process_item_logic(ctx.db, item_id);
+ return Ok();
+}
+
+SPACETIMEDB_PROCEDURE(Unit, fetch_and_process, ProcedureContext ctx, std::string url) {
+ auto result = ctx.http.get(url);
+ if (!result.is_ok()) {
+ LOG_ERROR("Request failed: " + result.error());
+ return Unit{};
+ }
+
+ auto& response = result.value();
+ if (response.status_code != 200) {
+ LOG_ERROR("HTTP status: " + std::to_string(response.status_code));
+ return Unit{};
+ }
+
+ auto body = response.body.to_string_utf8_lossy();
+ uint64_t item_id = std::stoull(body);
+
+ ctx.with_tx([&](TxContext& tx) {
+ process_item_logic(tx.db, item_id);
+ });
+
+ return Unit{};
+}
+```
+
diff --git a/docs/docs/00200-core-concepts/00200-functions/00500-views.md b/docs/docs/00200-core-concepts/00200-functions/00500-views.md
index 0603fa97dad..cc2845cabbe 100644
--- a/docs/docs/00200-core-concepts/00200-functions/00500-views.md
+++ b/docs/docs/00200-core-concepts/00200-functions/00500-views.md
@@ -225,6 +225,64 @@ fn players_for_level(ctx: &AnonymousViewContext) -> Vec {
Views can return either `Option` for at-most-one row or `Vec` for multiple rows, where `T` can be a table type or any product type.
+
+
+
+Use the `SPACETIMEDB_VIEW` macro:
+
+```cpp
+#include
+using namespace SpacetimeDB;
+
+struct Player {
+ uint64_t id;
+ Identity identity;
+ std::string name;
+};
+SPACETIMEDB_STRUCT(Player, id, identity, name)
+SPACETIMEDB_TABLE(Player, player, Public)
+FIELD_PrimaryKeyAuto(player, id)
+FIELD_Unique(player, identity)
+
+struct PlayerLevel {
+ uint64_t player_id;
+ uint64_t level;
+};
+SPACETIMEDB_STRUCT(PlayerLevel, player_id, level)
+SPACETIMEDB_TABLE(PlayerLevel, player_level, Public)
+FIELD_Unique(player_level, player_id)
+FIELD_Index(player_level, level)
+
+struct PlayerAndLevel {
+ uint64_t id;
+ std::string name;
+ uint64_t level;
+};
+SPACETIMEDB_STRUCT(PlayerAndLevel, id, name, level)
+
+// At-most-one row: return std::optional
+SPACETIMEDB_VIEW(std::optional, my_player, Public, ViewContext ctx) {
+ return ctx.db[player_identity].find(ctx.sender);
+}
+
+// Multiple rows: return std::vector
+SPACETIMEDB_VIEW(std::vector, players_for_level, Public, AnonymousViewContext ctx) {
+ std::vector results;
+
+ // Find all player levels with level == 2
+ for (auto player_lv : ctx.db[player_level_level].filter(uint64_t(2))) {
+ auto p = ctx.db[player_id].find(player_lv.player_id);
+ if (p) {
+ results.push_back(PlayerAndLevel{p->id, p->name, player_lv.level});
+ }
+ }
+
+ return results;
+}
+```
+
+Views can return either `std::optional` for at-most-one row or `std::vector` for multiple rows, where `T` can be a table type or any product type.
+
@@ -298,6 +356,16 @@ fn my_player(ctx: &ViewContext) -> Option {
}
```
+
+
+
+```cpp
+// Per-user: each client sees their own player
+SPACETIMEDB_VIEW(std::optional, my_player, Public, ViewContext ctx) {
+ return ctx.db[player_identity].find(ctx.sender);
+}
+```
+
@@ -377,6 +445,26 @@ fn high_scorers(ctx: &AnonymousViewContext) -> Vec {
}
```
+
+
+
+```cpp
+struct Player {
+ uint64_t id;
+ std::string name;
+ uint64_t score;
+};
+SPACETIMEDB_STRUCT(Player, id, name, score)
+SPACETIMEDB_TABLE(Player, player, Public)
+FIELD_PrimaryKeyAutoInc(player, id)
+FIELD_Index(player, score)
+
+// Shared: same high scorers for all clients
+SPACETIMEDB_VIEW(std::vector, high_scorers, Public, AnonymousViewContext ctx) {
+ return ctx.db[player_high_scorers_score].filter(range_from(uint64_t(1000))).collect();
+}
+```
+
@@ -554,6 +642,75 @@ fn entities_in_my_chunk(ctx: &ViewContext) -> Vec {
}
```
+
+
+
+```cpp
+struct Entity {
+ uint64_t id;
+ int32_t chunk_x;
+ int32_t chunk_y;
+ float local_x;
+ float local_y;
+ std::string entity_type;
+};
+SPACETIMEDB_STRUCT(Entity, id, chunk_x, chunk_y, local_x, local_y, entity_type)
+SPACETIMEDB_TABLE(Entity, entity, Public)
+FIELD_PrimaryKeyAutoInc(entity, id)
+FIELD_Index(entity, chunk_x)
+FIELD_Index(entity, chunk_y)
+
+// Track which chunks each player is subscribed to
+struct PlayerChunk {
+ uint64_t player_id;
+ int32_t chunk_x;
+ int32_t chunk_y;
+};
+SPACETIMEDB_STRUCT(PlayerChunk, player_id, chunk_x, chunk_y)
+SPACETIMEDB_TABLE(PlayerChunk, player_chunk, Public)
+FIELD_PrimaryKey(player_chunk, player_id)
+
+// Shared: all players in chunk (0,0) share this view
+SPACETIMEDB_VIEW(std::vector, entities_in_origin_chunk, Public, AnonymousViewContext ctx) {
+ std::vector results;
+
+ // All entities in chunk (0, 0) - shared by everyone viewing this chunk
+ for (auto entity : ctx.db[entity_chunk_x].filter(int32_t(0))) {
+ if (entity.chunk_y == 0) {
+ results.push_back(entity);
+ }
+ }
+
+ return results;
+}
+
+// Per-user: returns entities in the chunk the player is currently in
+SPACETIMEDB_VIEW(std::vector, entities_in_my_chunk, Public, ViewContext ctx) {
+ std::vector results;
+
+ // Find the player's current location
+ auto player_opt = ctx.db[player_identity].find(ctx.sender);
+ if (!player_opt) {
+ return results; // Player not found
+ }
+
+ // Find which chunk the player is in
+ auto chunk_opt = ctx.db[player_chunk_player_id].find(player_opt->id);
+ if (!chunk_opt) {
+ return results; // Player has no chunk assigned
+ }
+
+ // Return all entities in the player's chunk
+ for (auto entity : ctx.db[entity_chunk_x].filter(chunk_opt->chunk_x)) {
+ if (entity.chunk_y == chunk_opt->chunk_y) {
+ results.push_back(entity);
+ }
+ }
+
+ return results;
+}
+```
+