Skip to main content

Key architectural concepts

Host

A SpacetimeDB host is a server that hosts databases. You can run your own host, or use the SpacetimeDB maincloud. Many databases can run on a single host.

Database

A SpacetimeDB database is an application that runs on a host.

A database exports tables, which store data, and reducers, which allow clients to make requests.

A database's schema and business logic is specified by a piece of software called a module. Modules can be written in C# or Rust.

(Technically, a SpacetimeDB module is a WebAssembly module or JavaScript bundle, that imports a specific low-level WebAssembly ABI and exports a small number of special functions. However, the SpacetimeDB server-side libraries hide these low-level details. As a developer, writing a module is mostly like writing any other C# or Rust application, except for the fact that a special CLI tool is used to deploy the application.)

Table

A SpacetimeDB table is a SQL database table. Tables are declared in a module's native language. For instance, in C#, a table is declared like so:

#[spacetimedb::table(name = players, public)]
pub struct Player {
   #[primary_key]
   id: u64,
   name: String,
   age: u32,
   user: Identity,
}

The contents of a table can be read and updated by reducers. Tables marked public can also be read by clients.

Reducer

A reducer is a function exported by a database. Connected clients can call reducers to interact with the database. This is a form of remote procedure call.

A reducer can be written in Rust like so:

#[spacetimedb::reducer]
pub fn set_player_name(ctx: &spacetimedb::ReducerContext, id: u64, name: String) -> Result<(), String> {
   // ...
}

And a Rust client can call that reducer:

fn main() {
   // ...setup code, then...
   ctx.reducers.set_player_name(57, "Marceline".into());
}

These look mostly like regular function calls, but under the hood, the client sends a request over the internet, which the database processes and responds to.

The ReducerContext is a reducer's only mandatory parameter and includes information about the caller's identity. This can be used to authenticate the caller.

Reducers are run in their own separate and atomic database transactions. When a reducer completes successfully, the changes the reducer has made, such as inserting a table row, are committed to the database. However, if the reducer instead returns an error, or throws an exception, the database will instead reject the request and revert all those changes. That is, reducers and transactions are all-or-nothing requests. It's not possible to keep the first half of a reducer's changes and discard the last.

Transactions are only started by requests from outside the database. When a reducer calls another reducer directly, as in the example below, the changes in the called reducer does not happen in its own child transaction. Instead, when the nested reducer gracefully errors, and the overall reducer completes successfully, the changes in the nested one are still persisted.

#[spacetimedb::reducer]
pub fn hello(ctx: &spacetimedb::ReducerContext) -> Result<(), String> {
   if world(ctx).is_err() {
      other_changes(ctx);
   }
}

#[spacetimedb::reducer]
pub fn world(ctx: &spacetimedb::ReducerContext) -> Result<(), String> {
    clear_all_tables(ctx);
}

While SpacetimeDB doesn't support nested transactions, a reducer can schedule another reducer to run at an interval, or at a specific time.

See Reducers for more details about reducers.

Procedure

A procedure is a function exported by a database, similar to a reducer. Connected clients can call procedures. Procedures can perform additional operations not possible in reducers, including making HTTP requests to external services. However, procedures don't automatically run in database transactions, and must manually open and commit a transaction in order to read from or modify the database state.

Procedures are currently in beta, and their API may change in upcoming SpacetimeDB releases.

Because procedures are unstable, Rust modules that define them must opt in to the unstable feature in their Cargo.toml:

[dependencies]
spacetimedb = { version = "1.x", features = ["unstable"] }

Then, that module can define a procedure:

#[spacetimedb::procedure]
pub fn make_request(ctx: &mut spacetimedb::ProcedureContext) -> String {
    // ...
}

And a Rust client can call that procedure:

fn main() {
    // ...setup code, then...
    ctx.procedures.make_request();
}

A Rust client can also register a callback to run when a procedure call finishes, which will be invoked with that procedure's return value:

fn main() {
    // ...setup code, then...
    ctx.procedures.make_request_then(|ctx, res| {
        match res {
            Ok(string) => log::info!("Procedure `make_request` returned {string}"),
            Err(e) => log::error!("Procedure  `make_request` failed! {e:?}"),
        }
    })
}

See Procedures for more details about procedures.

View

A view is a read-only function exported by a database that computes and returns results from tables. Unlike reducers, views do not modify database state - they only query and return data. Views are useful for computing derived data, aggregations, or joining multiple tables before sending results to clients.

Views must be declared as public and accept only a context parameter. They can return either a single row or multiple rows. Like tables, views can be subscribed to and automatically update when their underlying data changes.

A view can be written in Rust like so:

#[spacetimedb::view(name = my_player, public)]
fn my_player(ctx: &spacetimedb::ViewContext) -> Option<Player> {
    ctx.db.player().identity().find(ctx.sender)
}

Views can be queried and subscribed to using SQL:

SELECT * FROM my_player;

See Views for more details about views.

Client

A client is an application that connects to a database. A client logs in using an identity and receives an connection id to identify the connection. After that, it can call reducers and query public tables.

Clients are written using the client-side SDKs. The spacetime CLI tool allows automatically generating code that works with the client-side SDKs to talk to a particular database.

Clients are regular software applications that developers can choose how to deploy (through Steam, app stores, package managers, or any other software deployment method, depending on the needs of the application.)

Identity

A SpacetimeDB Identity identifies someone interacting with a database. It is a long lived, public, globally valid identifier that will always refer to the same end user, even across different connections.

A user's Identity is attached to every reducer call they make, and you can use this to decide what they are allowed to do.

Modules themselves also have Identities. When you spacetime publish a module, it will automatically be issued an Identity to distinguish it from other modules. Your client application will need to provide this Identity when connecting to the host.

Identities are issued using the OpenID Connect specification. Database developers are responsible for issuing Identities to their end users. OpenID Connect lets users log in to these accounts through standard services like Google and Facebook.

Specifically, an identity is derived from the issuer and subject fields of a JSON Web Token (JWT) hashed together. The psuedocode for this is as follows:

def identity_from_claims(issuer: str, subject: str) -> [u8; 32]:
   hash1: [u8; 32] = blake3_hash(issuer + "|" + subject)
   id_hash: [u8; 26] = hash1[:26]
   checksum_hash: [u8; 32] = blake3_hash([
      0xC2,
      0x00,
      *id_hash
   ])
   identity_big_endian_bytes: [u8; 32] = [
      0xC2,
      0x00,
      *checksum_hash[:4],
      *id_hash
   ]
   return identity_big_endian_bytes

You can obtain a JWT from our turnkey identity provider SpacetimeAuth, or you can get one from any OpenID Connect compliant identity provider.

ConnectionId

A ConnectionId identifies client connections to a SpacetimeDB database.

A user has a single Identity, but may open multiple connections to your database. Each of these will receive a unique ConnectionId.

Energy

Energy is the currency used to pay for data storage and compute operations in a SpacetimeDB host.