SpacetimeDB is a transactional, in-memory relational database that lets you implement database-backed state machines via Rust reducers and table schemas. All data is stored in memory and persisted via an append-only Write-Ahead Log (WAL) called the Commitlog. Reducers are atomic, deterministic functions that execute inside the database, providing ACID guarantees.
In STK2ETH we use SpacetimeDB to store USSD menus, sessions, swaps, and audit logs.
Tables and Schema
Tables are declared using Rust structs annotated with the #[table(name = <table>)]
macro, which generates typed accessors. This provides compile-time guarantees about columns and helps reduce human errors in row operations.
Example:
#[table(name = ussd_session, public)]
#[derive(Clone, Debug)]
pub struct UssdSession {
#[primary_key]
session_id: String,
// ... other fields ...
}
Important tables in STK2ETH:
-
ussd_menu
,ussd_screen
,menu_item
: represent static menu structure loaded from a file. -
ussd_session
: session rows for active interactions. -
swap
: holds pending/executed swaps and transaction metadata. -
eth_audit_logs
: immutable audit trail for regulatory needs.
Reducer Semantics
Reducers are Rust functions annotated with #[reducer]
that take a &ReducerContext
as the first argument. They are applied deterministically and atomically, and can only return ()
or Result<(), E>
where E: Display
. This ensures state changes can be serialized consistently.
Example:
#[reducer]
pub fn set_name(ctx: &ReducerContext, name: String) -> Result<(), String> {
// reducer logic
}
Guidelines:
- Keep business logic testable: move pure logic (parsing, validation) into separate functions. Use reducers only for side-effects and DB writes.
- Use stable string error codes when returning
Err(String)
from reducers to allow the UI to map errors to screens. - Avoid long-running synchronous operations in reducers. Use a table (e.g.,
ussd_request
) to enqueue work for out-of-band workers when you need to call external services.
Deterministic Design and Side-Effect Management
Reducers must be deterministic and cannot perform direct network or filesystem I/O. External operations should be handled by enqueuing requests in a table, and a separate worker can process those requests and write results back to the database.
Pattern:
- Insert a
ussd_request
row describing the work. - An off-chain worker reads the request, performs the external call, and inserts result rows for reducers to consume.
Testing Reducers
- Unit test pure functions directly.
- Reducer tests should be small and deterministic. Use the SpacetimeDB test harness for reducers that require database access (when available).
Example Patterns
-
Validation: Pure function
validate_amount_value
+ reducer wrappervalidate_amount
that reads config, then returnsOk(())
orErr("amount_too_low")
. -
Enqueueing: Reducers insert
ussd_request
rows and return immediately.
Observability
Reducers should log important events (e.g., session created, swap created, request enqueued) using SpacetimeDB’s logging system. This helps with debugging and auditing state transitions.
SpacetimeDB offers a deterministic and transactional pattern for implementing state machines. Treat reducers as the boundary for state changes, keep logic testable, enqueue external interactions for off-chain workers, and use stable error codes for UI mapping.
Top comments (0)