Crates.io | spacetimedb |
lib.rs | spacetimedb |
version | 1.2.0 |
source | src |
created_at | 2022-09-20 07:25:35.574708+00 |
updated_at | 2025-06-17 16:41:38.623268+00 |
description | Easy support for interacting between SpacetimeDB and Rust. |
homepage | |
repository | |
max_upload_size | |
id | 669677 |
size | 187,163 |
SpacetimeDB allows using the Rust language to write server-side applications called modules. Modules, which run inside a relational database, have direct access to database tables, and expose public functions called reducers that can be invoked over the network. Clients connect directly to the database to read data.
Client Application SpacetimeDB
┌───────────────────────┐ ┌───────────────────────┐
│ │ │ │
│ ┌─────────────────┐ │ SQL Query │ ┌─────────────────┐ │
│ │ Subscribed Data │<─────────────────────│ Database │ │
│ └─────────────────┘ │ │ └─────────────────┘ │
│ │ │ │ ^ │
│ │ │ │ │ │
│ v │ │ v │
│ +─────────────────┐ │ call_reducer() │ ┌─────────────────┐ │
│ │ Client Code │─────────────────────>│ Module Code │ │
│ └─────────────────┘ │ │ └─────────────────┘ │
│ │ │ │
└───────────────────────┘ └───────────────────────┘
Rust modules are written with the the Rust Module Library (this crate). They are built using cargo and deployed using the spacetime
CLI tool. Rust modules can import any Rust crate that supports being compiled to WebAssembly.
(Note: Rust can also be used to write clients of SpacetimeDB databases, but this requires using a different library, the SpacetimeDB Rust Client SDK. See the documentation on clients for more information.)
This reference assumes you are familiar with the basics of Rust. If you aren't, check out Rust's excellent documentation. For a guided introduction to Rust Modules, see the Rust Module Quickstart.
SpacetimeDB modules have two ways to interact with the outside world: tables and reducers.
Tables store data and optionally make it readable by clients.
Reducers are functions that modify data and can be invoked by clients over the network. They can read and write data in tables, and write to a private debug log.
These are the only ways for a SpacetimeDB module to interact with the outside world. Calling functions from std::net
or std::fs
inside a reducer will result in runtime errors.
Declaring tables and reducers is straightforward:
# #[cfg(target_arch = "wasm32")] mod demo {
use spacetimedb::{table, reducer, ReducerContext, Table};
#[table(name = player)]
pub struct Player {
id: u32,
name: String
}
#[reducer]
fn add_person(ctx: &ReducerContext, id: u32, name: String) {
log::debug!("Inserting {name} with id {id}");
ctx.db.player().insert(Player { id, name });
}
# }
Note that reducers don't return data directly; they can only modify the database. Clients connect directly to the database and use SQL to query public tables. Clients can also subscribe to a set of rows using SQL queries and receive streaming updates whenever any of those rows change.
Tables and reducers in Rust modules can use any type that implements the [SpacetimeType
] trait.
To create a Rust module, install the spacetime
CLI tool in your preferred shell. Navigate to your work directory and run the following command:
spacetime init --lang rust my-project-directory
This creates a Cargo project in my-project-directory
with the following Cargo.toml
:
[package]
name = "spacetime-module"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
spacetimedb = "1.0.0"
log = "0.4"
This is a standard Cargo.toml
, with the exception of the line crate-type = ["cdylib"]
.
This line is important: it allows the project to be compiled to a WebAssembly module.
The project's lib.rs
will contain the following skeleton:
# #[cfg(target_arch = "wasm32")] mod demo {
use spacetimedb::{ReducerContext, Table};
#[spacetimedb::table(name = person)]
pub struct Person {
name: String
}
#[spacetimedb::reducer(init)]
pub fn init(_ctx: &ReducerContext) {
// Called when the module is initially published
}
#[spacetimedb::reducer(client_connected)]
pub fn identity_connected(_ctx: &ReducerContext) {
// Called everytime a new client connects
}
#[spacetimedb::reducer(client_disconnected)]
pub fn identity_disconnected(_ctx: &ReducerContext) {
// Called everytime a client disconnects
}
#[spacetimedb::reducer]
pub fn add(ctx: &ReducerContext, name: String) {
ctx.db.person().insert(Person { name });
}
#[spacetimedb::reducer]
pub fn say_hello(ctx: &ReducerContext) {
for person in ctx.db.person().iter() {
log::info!("Hello, {}!", person.name);
}
log::info!("Hello, World!");
}
# }
This skeleton declares a table, some reducers, and some lifecycle reducers.
To compile the project, run the following command:
spacetime build
SpacetimeDB requires a WebAssembly-compatible Rust toolchain. If the spacetime
cli finds a compatible version of rustup
that it can run, it will automatically install the wasm32-unknown-unknown
target and use it to build your application. This can also be done manually using the command:
rustup target add wasm32-unknown-unknown
If you are managing your Rust installation in some other way, you will need to install the wasm32-unknown-unknown
target yourself.
To build your application and upload it to the public SpacetimeDB network, run:
spacetime login
And then:
spacetime publish [MY_DATABASE_NAME]
For example:
spacetime publish silly_demo_app
When you publish your module, a database named silly_demo_app
will be created with the requested tables, and the module will be installed inside it.
The output of spacetime publish
will end with a line:
Created new database with name: <name>, identity: <hex string>
This name is the human-readable name of the created database, and the hex string is its [Identity
]. These distinguish the created database from the other databases running on the SpacetimeDB network. They are used when administering the application, for example using the spacetime logs <DATABASE_NAME>
command. You should probably write the database name down in a text file so that you can remember it.
After modifying your project, you can run:
spacetime publish <DATABASE_NAME>
to update the module attached to your database. Note that SpacetimeDB tries to automatically migrate your database schema whenever you run spacetime publish
.
You can also generate code for clients of your module using the spacetime generate
command. See the client SDK documentation for more information.
Under the hood, SpacetimeDB modules are WebAssembly modules that import a specific WebAssembly ABI and export a small number of special functions. This is automatically configured when you add the spacetime
crate as a dependency of your application.
The SpacetimeDB host is an application that hosts SpacetimeDB databases. Its source code is available under the Business Source License with an Additional Use Grant. You can run your own host, or you can upload your module to the public SpacetimeDB network. The network will create a database for you and install your module in it to serve client requests.
The spacetime publish [DATABASE_IDENTITY]
command compiles a module and uploads it to a SpacetimeDB host. After this:
DATABASE_IDENTITY
.
#[init]
reducer.From the perspective of clients, this process is seamless. Open connections are maintained and subscriptions continue functioning. Automatic migrations forbid most table changes except for adding new tables, so client code does not need to be recompiled. However:
Clients may witness a brief interruption in the execution of scheduled reducers (for example, game loops.)
New versions of a module may remove or change reducers that were previously present. Client code calling those reducers will receive runtime errors.
Tables are declared using the #[table(name = table_name)]
macro.
This macro is applied to a Rust struct with named fields. All of the fields of the table must implement [SpacetimeType
].
The resulting type is used to store rows of the table. It is normal struct type. Row values are not special -- operations on row types do not, by themselves, modify the table. Instead, a ReducerContext
is needed to get a handle to the table.
# #[cfg(target_arch = "wasm32")] mod demo {
use spacetimedb::{table, reducer, ReducerContext, Table, UniqueColumn};
/// A `Person` is a row of the table `person`.
#[table(name = person, public)]
pub struct Person {
#[primary_key]
#[auto_inc]
id: u64,
#[index(btree)]
name: String,
}
// `Person` is a normal Rust struct type.
// Operations on a `Person` do not, by themselves, do anything.
// The following function does not interact with the database at all.
fn do_nothing() {
// Creating a `Person` DOES NOT modify the database.
let mut person = Person { id: 0, name: "Joe Average".to_string() };
// Updating a `Person` DOES NOT modify the database.
person.name = "Joanna Average".to_string();
// Dropping a `Person` DOES NOT modify the database.
drop(person);
}
// To interact with the database, you need a `ReducerContext`,
// which is provided as the first parameter of any reducer.
#[reducer]
fn do_something(ctx: &ReducerContext) {
// `ctx.db.{table_name}()` gets a handle to a database table.
let person: &person__TableHandle = ctx.db.person();
// The following inserts a row into the table:
let mut example_person = person.insert(Person { id: 0, name: "Joe Average".to_string() });
// `person` is a COPY of the row stored in the database.
// If we update it:
example_person.name = "Joanna Average".to_string();
// Our copy is now updated, but the database's copy is UNCHANGED.
// To push our change through, we can call `UniqueColumn::update()`:
example_person = person.id().update(example_person);
// Now the database and our copy are in sync again.
// We can also delete the row in the database using `UniqueColumn::delete()`.
person.id().delete(&example_person.id);
}
# }
(See reducers for more information on declaring reducers.)
This library generates a custom API for each table, depending on the table's name and structure.
All tables support getting a handle implementing the [Table
] trait from a [ReducerContext
], using:
ctx.db.{table_name}()
For example,
ctx.db.person()
The [Table
] trait provides:
Table::insert
]Table::try_insert
]Table::delete
]Table::iter
]Table::count
]Tables' constraints and indexes generate additional accessors.
By default, tables are considered private. This means that they are only readable by the database owner and by reducers. Reducers run inside the database, so clients cannot see private tables at all.
Using the #[table(name = table_name, public)]
flag makes a table public. Public tables are readable by all clients. They can still only be modified by reducers.
# #[cfg(target_arch = "wasm32")] mod demo {
use spacetimedb::table;
// The `enemies` table can be read by all connected clients.
#[table(name = enemy, public)]
pub struct Enemy {
/* ... */
}
// The `loot_items` table is invisible to clients, but not to reducers.
#[table(name = loot_item)]
pub struct LootItem {
/* ... */
}
# }
(Note that, when run by the module owner, the spacetime sql <SQL_QUERY>
command can also read private tables. This is for debugging convenience. Only the module owner can see these tables. This is determined by the Identity
stored by the spacetime login
command. Run spacetime login show
to print your current logged-in Identity
.)
To learn how to subscribe to a public table, see the client SDK documentation.
Columns of a table (that is, fields of a #[table]
struct) can be annotated with #[unique]
or #[primary_key]
. Multiple columns can be #[unique]
, but only one can be #[primary_key]
. For example:
# #[cfg(target_arch = "wasm32")] mod demo {
use spacetimedb::table;
type SSN = String;
type Email = String;
#[table(name = citizen)]
pub struct Citizen {
#[primary_key]
id: u64,
#[unique]
ssn: SSN,
#[unique]
email: Email,
name: String,
}
# }
Every row in the table Person
must have unique entries in the id
, ssn
, and email
columns. Attempting to insert multiple Person
s with the same id
, ssn
, or email
will fail. (Either via panic, with [Table::insert
], or via a Result::Err
, with [Table::try_insert
].)
Any #[unique]
or #[primary_key]
column supports getting a [UniqueColumn
] from a [ReducerContext
] using:
ctx.db.{table}().{unique_column}()
For example,
ctx.db.person().ssn()
[UniqueColumn
] provides:
UniqueColumn::find
]UniqueColumn::delete
]UniqueColumn::update
]Notice that updating a row is only possible if a row has a unique column -- there is no update
method in the base [Table
] trait. SpacetimeDB has no notion of rows having an "identity" aside from their unique / primary keys.
The #[primary_key]
annotation implies #[unique]
annotation, but avails additional methods in the client-side SDKs.
It is not currently possible to mark a group of fields as collectively unique.
Filtering on unique columns is only supported for a limited number of types.
Columns can be marked #[auto_inc]
. This can only be used on integer types (i32
, u8
, etc.)
When inserting into a table with an #[auto_inc]
column, if the annotated column is set to zero (0
), the database will automatically overwrite that zero with an atomically increasing value.
[Table::insert
] and [Table::try_insert
] return rows with #[auto_inc]
columns set to the values that were actually written into the database.
Note: The auto_inc
number generator is not transactional. See the SEQUENCE section for more details.
# #[cfg(target_arch = "wasm32")] mod demo {
use spacetimedb::{table, reducer, ReducerContext, Table};
#[table(name = example)]
struct Example {
#[auto_inc]
field: u32
}
#[reducer]
fn insert_auto_inc_example(ctx: &ReducerContext) {
for i in 1..=10 {
// These will have distinct, unique values
// at rest in the database, since they
// are inserted with the sentinel value 0.
let actual = ctx.db.example().insert(Example { field: 0 });
assert!(actual.field != 0);
}
}
# }
auto_inc
is often combined with unique
or primary_key
to automatically assign unique integer identifiers to rows.
SpacetimeDB supports both single- and multi-column B-Tree indexes.
Indexes are declared using the syntax:
#[table(..., index(name = my_index, btree(columns = [a, b, c]))]
.
For example:
# #[cfg(target_arch = "wasm32")] mod demo {
use spacetimedb::table;
#[table(name = paper, index(name = url_and_country, btree(columns = [url, country])))]
struct Paper {
url: String,
country: String,
venue: String
}
# }
Multiple indexes can be declared, separated by commas.
Single-column indexes can also be declared using the
column attribute.
For example:
# #[cfg(target_arch = "wasm32")] mod demo {
use spacetimedb::table;
#[table(name = paper)]
struct Paper {
url: String,
country: String,
#[index(btree)]
venue: String
}
# }
Any index supports getting a [RangedIndex
] using ctx
.db.{table}().{index}()
. For example, ctx.db.person().name()
.
[RangedIndex
] provides:
RangedIndex::filter
]RangedIndex::delete
]Only types which implement FilterableValue
may be used as index keys.
Reducers are declared using the #[reducer]
macro.
#[reducer]
is always applied to top level Rust functions. Arguments of reducers must implement [SpacetimeType
]. Reducers can either return nothing, or return a Result<(), E>
, where E
implements [std::fmt::Display
].
# #[cfg(target_arch = "wasm32")] mod demo {
use spacetimedb::{reducer, ReducerContext};
use std::fmt;
#[reducer]
fn give_player_item(
ctx: &ReducerContext,
player_id: u64,
item_id: u64
) -> Result<(), String> {
/* ... */
# Ok(())
}
# }
Every reducer runs inside a database transaction. This means that reducers will not observe the effects of other reducers modifying the database while they run. If a reducer fails, all of its changes to the database will automatically be rolled back. Reducers can fail by panicking or by returning an Err
.
ReducerContext
TypeReducers have access to a special [ReducerContext
] parameter. This parameter allows reading and writing the database attached to a module. It also provides some additional functionality, like generating random numbers and scheduling future operations.
[ReducerContext
] provides access to the database tables via the .db
field. The #[table]
macro generates traits that add accessor methods to this field.
log
crateSpacetimeDB Rust modules have built-in support for the log crate. All modules automatically install a suitable logger when they are first loaded by SpacetimeDB. (At time of writing, this happens here). Log macros can be used anywhere in module code, and log outputs of a running module can be inspected using the spacetime logs
command:
spacetime logs <DATABASE_IDENTITY>
A small group of reducers are called at set points in the module lifecycle. These are used to initialize the database and respond to client connections. See Lifecycle Reducers.
Reducers can schedule other reducers to run asynchronously. This allows calling the reducers at a particular time, or at repeating intervals. This can be used to implement timers, game loops, and maintenance tasks. See Scheduled Reducers.
When you spacetime publish
a module that has already been published using spacetime publish <DATABASE_NAME_OR_IDENTITY>
,
SpacetimeDB attempts to automatically migrate your existing database to the new schema. (The "schema" is just the collection
of tables and reducers you've declared in your code, together with the types they depend on.) This form of migration is limited and only supports a few kinds of changes.
On the plus side, automatic migrations usually don't break clients. The situations that may break clients are documented below.
The following changes are always allowed and never breaking:
#[auto_inc]
annotations.#[unique]
annotations.The following changes are allowed, but may break clients:
#[primary_key]
annotations. Non-updated clients will still use the old #[primary_key]
as a unique key in their local cache, which can result in non-deterministic behavior when updates are received.SELECT Employee.*
FROM Employee JOIN Dept
ON Employee.DeptName = Dept.DeptName
)
For performance reasons, SpacetimeDB will only allow this kind of subscription query if there are indexes on Employee.DeptName
and Dept.DeptName
. Removing either of these indexes will invalidate this subscription query, resulting in client-side runtime errors.The following changes are forbidden without a manual migration:
#[unique]
or #[primary_key]
constraints. This could result in existing tables being in an invalid state.Currently, manual migration support is limited. The spacetime publish --clear-database <DATABASE_IDENTITY>
command can be used to COMPLETELY DELETE and reinitialize your database, but naturally it should be used with EXTREME CAUTION.