Getting Started with SpacetimeDB Development: Build Serverless Relational Databases with Rust, C#, or TypeScript
SpacetimeDB is a serverless relational database that merges persistent storage with application logic, allowing developers to write tables and reducers in Rust, C#, or TypeScript that compile to WebAssembly while clients connect directly via generated SDKs to execute atomic transactions.
Getting started with SpacetimeDB development requires understanding its architecture as a unified database and application server. Instead of managing separate backend infrastructure, developers write modules—compiled WebAssembly bundles that define both schema and business logic—while the Host handles persistence, transaction enforcement, and real-time client synchronization. This guide explores the core components, CLI workflow, and code patterns from the clockworklabs/SpacetimeDB repository to help you build and deploy your first application.
Understanding the SpacetimeDB Architecture
SpacetimeDB operates through three primary components that eliminate traditional backend boilerplate. According to docs/intro/key-architecture.md, the Host runs one or many databases, manages storage durability, and handles energy billing for cloud deployments. Each Database represents an instance of a Module—a compiled WebAssembly (or JavaScript bundle) that exports a small ABI defining tables, reducers, and views.
The Module serves as your application layer. When you write code in Rust, C#, or TypeScript, you define Tables using SQL-style relational schemas and Reducers—server-side functions that mutate state within atomic transactions. The module code lives in your project directory (typically spacetimedb/src/lib.rs for Rust projects) and compiles through the procedural macros defined in crates/bindings-macro/src/lib.rs. The VM component, implemented in crates/vm/src/lib.rs, executes these WebAssembly modules, resolves reducer calls, and enforces strict transaction semantics.
Views provide read-only derived queries that clients can subscribe to, while Procedures (currently in Beta) allow non-transactional work such as external HTTP calls. Client applications interact with these components through SDKs generated from your module's metadata, implemented in crates/bindings/src/lib.rs, which expose type-safe methods for calling reducers and querying tables.
Installing the CLI and Bootstrapping Your Project
The spacetime CLI orchestrates the entire development workflow, from local testing to cloud deployment. The CLI implementation resides in crates/cli/src/lib.rs, with the dev subcommand specifically located in crates/cli/src/tasks/dev.rs.
Install the CLI using the official installer:
curl -sSf https://install.spacetimedb.com | sh
Create your first project using a built-in template:
spacetime dev --template basic-rs my-app
This command performs several operations defined in the CLI source: it boots a local Host, initializes a new Rust project structure, compiles the starter module, publishes it to the local database, and generates client bindings. The generated project contains a spacetimedb/src/lib.rs file with sample tables and reducers, plus a client/ directory with SDK bindings.
Writing Your First Module: Tables and Reducers
A SpacetimeDB module declares tables using procedural macros and implements business logic through reducer functions. The following example from docs/quickstarts/rust.md demonstrates the minimal structure:
use spacetimedb::{ReducerContext, Table};
#[spacetimedb::table(name = person, public)]
pub struct Person {
name: String,
}
#[spacetimeib::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!");
}
The #[spacetimedb::table] macro, processed by the macro crate at crates/bindings-macro/src/lib.rs, registers the Person struct as a public table accessible to clients. The #[spacetimedb::reducer] attribute marks functions that execute within database transactions—every reducer call is atomic, ensuring data consistency.
For TypeScript developers, the equivalent module structure uses:
import { table, t, reducer } from "spacetimedb/server";
export const players = table(
{ name: "players", public: true },
{
id: t.u64().primaryKey(),
name: t.string(),
}
);
reducer("set_name", { id: t.u64(), name: t.string() }, (ctx, { id, name }) => {
const player = ctx.db.players.id.find(id);
if (player) player.name = name;
});
Interacting with Your Database
During development, the CLI provides multiple interfaces for testing your module. When running spacetime dev, the system watches for file changes and automatically recompiles and pushes updates to your local database.
Invoke reducers directly from the command line:
# Insert a row via the add reducer
spacetime call my-spacetime-app add Alice
# Query the table using SQL syntax
spacetime sql my-spacetime-app "SELECT * FROM person"
# → name
# → "Alice"
# Execute a reducer that produces logs
spacetime call my-spacetime-app say_hello
spacetime logs my-spacetime-app
# → INFO: Hello, Alice!
# → INFO: Hello, World!
For application integration, use the generated client SDKs. After running spacetime dev, the bindings appear in my-app/client/src/module_bindings/. The following Rust client example connects to the local development server and invokes reducers:
use my_spacetime_app::module_bindings::SpacetimeDbClient;
#[tokio::main]
async fn main() {
// Connect to localhost dev server
let mut client = SpacetimeDbClient::connect().await.unwrap();
// Call the add reducer with type safety
client.reducers.add("Bob".into()).await.unwrap();
// Query the person table directly
let rows = client.tables.person().await.unwrap();
println!("People in DB: {:?}", rows);
}
The client SDK abstracts the WebSocket communication handled by crates/client-api/src/lib.rs, providing native method calls that map directly to your module's reducers and tables.
Deploying to Production
When your module is ready for production, the CLI handles deployment to SpacetimeDB's cloud infrastructure. The publish command compiles your module in release mode and uploads it to a managed Host:
spacetime publish
Cloud-hosted databases scale automatically using SpacetimeDB's energy-based billing model, where the Host manages storage persistence and client connection streaming without requiring server configuration. The same module binary runs both locally during development and in production, ensuring consistent behavior across environments.
Summary
- SpacetimeDB eliminates the traditional backend layer by merging database persistence with application logic in WebAssembly modules.
- Modules are written in Rust, C#, or TypeScript and define Tables (schema) and Reducers (transactional business logic) using procedural macros from
crates/bindings-macro/src/lib.rs. - The
spacetimeCLI provides a complete development workflow: project generation (spacetime dev), local testing (spacetime call,spacetime sql), and cloud deployment (spacetime publish). - Client SDKs generated in
crates/bindings/src/lib.rsprovide type-safe connections to the database, allowing direct reducer invocation and table subscription without REST API boilerplate. - The VM (
crates/vm/src/lib.rs) enforces atomic transaction semantics for all reducer calls, ensuring data consistency across concurrent client connections.
Frequently Asked Questions
What programming languages does SpacetimeDB support for module development?
SpacetimeDB officially supports Rust, C#, and TypeScript for writing modules. The source code includes quick-start guides for each language in docs/quickstarts/, with Rust being the most mature implementation. Regardless of the language you choose, the module compiles to WebAssembly (or a JavaScript bundle) and exports the same ABI, allowing interoperability with client SDKs generated for Rust, C#, TypeScript, C++, and Unity.
What is the difference between a Reducer and a Procedure in SpacetimeDB?
A Reducer is an atomic, transactional server-side function that mutates database state—every reducer call operates as a single ACID transaction, and the VM in crates/vm/src/lib.rs ensures that either all changes apply or none do. A Procedure (currently in Beta) performs non-transactional work such as external HTTP calls or side effects that should not roll back if the transaction fails. Use reducers for data mutations and procedures for external integrations.
How do client SDKs connect to a SpacetimeDB database?
Client SDKs generated by the CLI provide native bindings that communicate via WebSocket connections managed by crates/client-api/src/lib.rs. When you run spacetime dev, the CLI generates type-safe methods in client/src/module_bindings/ that map directly to your module's reducers and tables. Clients invoke reducers using ctx.reducers.name(args) and subscribe to table updates through the streaming protocol, receiving real-time delta updates without polling.
Can I run SpacetimeDB locally without a cloud account?
Yes. The spacetime dev command spins up a local Host on your machine, implemented in the CLI tasks at crates/cli/src/tasks/dev.rs. This local host provides the full database functionality—including persistence, transaction management, and client SDK generation—without requiring internet connectivity or cloud credentials. Use this environment for development and testing before deploying with spacetime publish.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →