Welcome back, aspiring real-time architect! In Chapter 1, we explored the “why” behind SpaceTimeDB, understanding its unique approach to unifying database, backend logic, and real-time synchronization. Now, it’s time to roll up our sleeves and dive into the “how.”

This chapter is your hands-on initiation into the SpaceTimeDB universe. We’ll guide you through setting up your development environment, creating your very first SpaceTimeDB project, defining a simple database schema, and writing server-side logic that modifies your data. By the end, you’ll have a running SpaceTimeDB instance on your local machine, ready to power real-time applications. Get ready to build, learn, and have some fun!

The SpaceTimeDB CLI: Your Command Center

At the heart of SpaceTimeDB development is the Command Line Interface (CLI). Think of it as your primary tool for everything from creating new projects and compiling your backend logic to running your local SpaceTimeDB instance and interacting with your database. The CLI streamlines the entire development workflow, making it easy to manage your SpaceTimeDB applications.

Understanding the SpaceTimeDB Development Workflow

Before we start typing commands, let’s get a mental picture of the typical SpaceTimeDB development cycle. It’s a bit different from traditional backend development, but incredibly efficient once you get the hang of it:

  1. Initialize Project: Use the CLI to scaffold a new SpaceTimeDB project. This sets up the basic directory structure and configuration files.
  2. Define Schema: Describe your application’s data structure using Rust types. These types become your database tables.
  3. Implement Reducers: Write Rust functions (called “reducers”) that define how your application’s state can change. These are your server-side “API endpoints” or “game actions.”
  4. Run Local Instance: Start a local SpaceTimeDB server using the CLI. It compiles your Rust code, creates the database, and watches for changes.
  5. Connect Clients: Develop frontend applications (web, mobile, game engines) that connect to your SpaceTimeDB instance and call your reducers to modify state, and subscribe to tables to receive real-time updates.

Here’s a simplified visual representation of this workflow:

flowchart TD A[Start] --> B[Install SpaceTimeDB CLI] B --> C[Create New Project] C --> D[Define Schema] D --> E[Implement Reducers] E --> F[Run SpaceTimeDB Local Server] F --> G[Connect Client Application] G --> H[Client Calls Reducers] H --> I[SpaceTimeDB Updates State] I --> J[SpaceTimeDB Notifies Clients] J --> G

Step-by-Step Implementation: Building Your First Project

Let’s get our hands dirty!

1. Prerequisites: What You’ll Need

SpaceTimeDB leverages Rust for its server-side logic. So, the first thing you’ll need is a working Rust environment.

  • Rust and Cargo: If you don’t have Rust installed, the easiest way is through rustup. Open your terminal and run:
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    
    Follow the on-screen instructions. This will install rustc (the Rust compiler) and cargo (Rust’s package manager and build tool).
    • Version Check: After installation, verify with:
      rustc --version
      cargo --version
      
      As of 2026-03-14, you should ideally be on a stable Rust version like Rust 1.77.x or newer.

2. Installing the SpaceTimeDB CLI

Now, let’s install the core tool: the SpaceTimeDB CLI. This is a single binary that simplifies your development.

  • Installation Command: Open your terminal and run:

    cargo install spacetimedb-cli
    

    This command uses Cargo to fetch and compile the spacetimedb-cli package from crates.io and installs it to your Cargo bin directory (usually ~/.cargo/bin). Make sure this directory is in your system’s PATH.

  • Verifying Installation: Once installed, confirm it’s working by checking its version:

    spacetime --version
    

    You should see output similar to spacetime-cli v2.2.0 (or a later v2.x version as of 2026-03-14). This confirms the CLI is correctly installed and accessible.

3. Creating Your First SpaceTimeDB Project

With the CLI ready, let’s scaffold a new project. We’ll create a simple “To-Do List” backend.

  1. Choose a Directory: Navigate to a directory where you want to create your project.

    cd ~/projects
    
  2. Create the Project: Use the spacetime new command:

    spacetime new my_todo_app
    

    The CLI will create a new directory named my_todo_app and populate it with initial files.

  3. Explore the Project Structure: Navigate into your new project:

    cd my_todo_app
    

    Take a look around. You’ll see something like this:

    my_todo_app/
    ├── src/
    │   └── lib.rs
    └── Spacetime.toml
    
    • src/lib.rs: This is where your Rust code for defining tables and reducers will live. It’s the “brain” of your SpaceTimeDB backend.
    • Spacetime.toml: This is the SpaceTimeDB configuration file. It tells the CLI how to build and run your project.

4. Understanding Spacetime.toml

Open Spacetime.toml in your favorite code editor. It’s a TOML file that configures your SpaceTimeDB instance.

# Spacetime.toml
[spacetime]
module_name = "my_todo_app"
# The full path to the Rust module that defines your tables and reducers.
# This usually points to your `src/lib.rs` file.
module_path = "src/lib.rs"

# [client_api]
# This section is for configuring client API generation,
# which we'll cover in a later chapter.
# For now, we'll leave it commented out or empty.
  • module_name: This is the name of your SpaceTimeDB module. It’s typically the same as your project directory name.
  • module_path: This specifies the path to your main Rust source file (lib.rs) where your database schema and logic are defined.

For now, these defaults are perfect. We’ll revisit this file in future chapters for advanced configurations like client API generation.

5. Defining Your First Schema: The Todo Table

Now, let’s define our first database table. In SpaceTimeDB, you define your tables using Rust structs.

Open src/lib.rs and replace its contents with the following:

// src/lib.rs
use spacetimedb::{spacetimedb, Identity, ReducerContext, SpacetimeType, SpacetimeTable};

// 1. Define your table as a Rust struct.
//    The `#[derive(...)]` macros are crucial for SpaceTimeDB to recognize this as a table.
#[spacetimedb(table)]
#[derive(SpacetimeType, SpacetimeTable)]
pub struct Todo {
    // 2. Define fields for your table.
    //    `#[primarykey]` designates this field as the unique identifier for each row.
    #[primarykey]
    pub id: u32,
    pub description: String,
    pub completed: bool,
    pub created_by: Identity, // Stores the identity of the user who created the todo.
}

// We'll add our reducer here next!

Let’s break down what we just added:

  • use spacetimedb::{...}: This line imports necessary traits and macros from the spacetimedb crate.
  • #[spacetimedb(table)]: This attribute macro tells SpaceTimeDB that the Todo struct should be treated as a database table. It’s essential!
  • #[derive(SpacetimeType, SpacetimeTable)]: These derive macros implement traits required by SpaceTimeDB for serialization/deserialization and table management.
    • SpacetimeType: Allows the struct to be used as a type within SpaceTimeDB’s system.
    • SpacetimeTable: Provides the necessary methods for the struct to function as a table.
  • pub struct Todo { ... }: This defines our Todo table.
  • #[primarykey]: This attribute marks the id field as the primary key. Every table must have exactly one primary key. It ensures each Todo item has a unique id.
  • pub id: u32: An unsigned 32-bit integer for the todo’s ID.
  • pub description: String: A String to hold the task description.
  • pub completed: bool: A boolean to track if the task is done.
  • pub created_by: Identity: This is a special SpaceTimeDB type that represents the unique identifier of a connected client. This is incredibly powerful for tracking who performed an action or owns data!

Why Rust for Schema? By defining your schema directly in Rust, you get strong type safety from the very beginning. Your database schema and your backend logic are defined in the same language, in the same project, leading to fewer mismatches and a more robust system.

6. Implementing Your First Reducer: create_todo

Now that we have a Todo table, how do we add data to it? This is where reducers come in. Reducers are functions defined in your src/lib.rs that encapsulate all the logic for modifying your SpaceTimeDB’s state. When a client wants to change something, they “call” a reducer.

Add the following code below your Todo struct in src/lib.rs:

// src/lib.rs (continued)
// ... (your Todo struct code here) ...

// 3. Define a reducer function.
//    `#[spacetimedb(reducer)]` marks this function as a reducer callable by clients.
#[spacetimedb(reducer)]
pub fn create_todo(ctx: ReducerContext, id: u32, description: String) -> Result<(), String> {
    // 4. Access the Identity of the client calling this reducer.
    let creator_identity = ctx.identity();

    // 5. Check if a todo with this ID already exists.
    if Todo::filter_by_id(&id).count() > 0 {
        return Err(format!("Todo with ID {} already exists.", id));
    }

    // 6. Insert a new Todo item into the database.
    Todo::insert(Todo {
        id,
        description,
        completed: false,
        created_by: creator_identity,
    });

    // 7. Return Ok(()) for success, or an Err(String) for failure.
    Ok(())
}

Let’s unpack this reducer:

  • #[spacetimedb(reducer)]: This attribute macro is crucial. It tells SpaceTimeDB that create_todo is a function that clients can invoke to change the database state.
  • pub fn create_todo(ctx: ReducerContext, id: u32, description: String) -> Result<(), String>:
    • ctx: ReducerContext: Every reducer receives a ReducerContext. This provides access to important information about the current call, such as the Identity of the client making the request.
    • id: u32, description: String: These are the arguments that a client will pass when calling this reducer. They become the data for our new Todo.
    • -> Result<(), String>: Reducers typically return a Result. Ok(()) indicates success, and Err(String) indicates an error with a message.
  • let creator_identity = ctx.identity();: We extract the Identity of the client who called this reducer. This is a powerful feature for security and data ownership.
  • if Todo::filter_by_id(&id).count() > 0 { ... }: Before inserting, we check if a todo with the given id already exists. Todo::filter_by_id() is a generated helper function that allows querying the Todo table by its primary key.
  • Todo::insert(Todo { ... });: This is how you add a new row to the Todo table. You create an instance of your Todo struct and call the static insert method on the Todo type.
  • Ok(()): If everything goes well, we return Ok(()) to signal success.

Why Reducers? Reducers centralize your application’s logic. All state changes must go through a reducer. This ensures:

  • Determinism: Given the same initial state and the same sequence of reducer calls, the final state will always be the same. This is key for SpaceTimeDB’s consistency model.
  • Validation: You can put all your input validation and business logic directly in your reducers.
  • Security: By controlling what actions are allowed (via reducers), you enforce your application’s rules on the server.

7. Running Your SpaceTimeDB Local Server

With our schema and reducer defined, it’s time to bring our SpaceTimeDB instance to life!

  1. Run the Development Server: In your my_todo_app project directory, open your terminal and run:

    spacetime dev
    

    The spacetime dev command does several things:

    • It compiles your Rust code (src/lib.rs).
    • It starts a local SpaceTimeDB server (typically listening on ws://localhost:3000).
    • It watches your src/lib.rs file for changes. If you save changes, it will automatically recompile and restart the server, providing a fast development loop.

    You should see output indicating compilation success and the server starting:

    Compiling my_todo_app v0.1.0 (~/projects/my_todo_app)
    Finished dev [unoptimized + debuginfo] target(s) in X.XXs
    Running `target/debug/my_todo_app`
    [INFO] SpacetimeDB server started on ws://localhost:3000
    [INFO] Watching for changes in src/lib.rs...
    

    Keep this terminal window open; your SpaceTimeDB server is now running!

8. Interacting with the SpaceTimeDB Console

While your server is running, open another terminal window, navigate back to your my_todo_app directory, and let’s use the spacetime console to interact with it.

spacetime console

This will open an interactive console that connects to your local SpaceTimeDB instance. You’ll see a prompt like spacetime-console>.

  • Calling the create_todo reducer: Let’s add our first todo item!

    spacetime-console> call create_todo 1 "Learn SpaceTimeDB"
    

    You should see output indicating success:

    Reducer 'create_todo' called successfully.
    

    Let’s add another one:

    spacetime-console> call create_todo 2 "Build a real-time app"
    
  • Querying the Todo table: Now, let’s see if our todos are actually in the database.

    spacetime-console> subscribe Todo
    

    This command “subscribes” your console to the Todo table, meaning you’ll get all current data and any future updates. You should immediately see the data you just inserted:

    [
        {
            "id": 1,
            "description": "Learn SpaceTimeDB",
            "completed": false,
            "created_by": "0x..." // Your client's identity
        },
        {
            "id": 2,
            "description": "Build a real-time app",
            "completed": false,
            "created_by": "0x..."
        }
    ]
    

    The created_by field will show a hexadecimal string, which is the unique Identity of your spacetime console session. Pretty neat, right?

    You can exit the console by typing exit or pressing Ctrl+D.

Mini-Challenge: Extend Your To-Do App!

You’ve successfully set up your environment, defined a table, written a reducer, and interacted with your SpaceTimeDB instance. Now, let’s solidify that knowledge with a small challenge.

Challenge: Modify your Todo table and create_todo reducer to include a new field: due_date: Option<String>.

  1. Add due_date to the Todo struct: Make it an Option<String> so that a due date is optional.
  2. Update the create_todo reducer: Modify its signature to accept Option<String> for the due_date. Update the Todo::insert call to include this new field.
  3. Test with spacetime console: Restart your spacetime dev server (it should auto-restart if you save changes). Then, in spacetime console, try calling create_todo with and without a due date:
    • call create_todo 3 "Buy groceries" "2026-03-15"
    • call create_todo 4 "Walk the dog" None
    • Then subscribe Todo again to see your updated table.

Hint: Remember that Option<String> in Rust can be Some("your_date_string".to_string()) or None.

What to Observe/Learn:

  • How easily you can evolve your schema.
  • How reducers adapt to schema changes.
  • How to handle optional fields in Rust and SpaceTimeDB.

Common Pitfalls & Troubleshooting

  1. spacetime command not found:
    • Issue: Your system’s PATH environment variable might not include Cargo’s bin directory (~/.cargo/bin).
    • Solution: Ensure you’ve sourced your shell’s profile after installing Rust (e.g., source ~/.bashrc or source ~/.zshrc). You might need to add export PATH="$HOME/.cargo/bin:$PATH" to your shell configuration file.
  2. Compilation Errors in spacetime dev:
    • Issue: Rust syntax errors, missing use statements, or incorrect #[spacetimedb(...)] attributes.
    • Solution: Carefully read the error messages from the Rust compiler. They are usually very descriptive and point to the exact line and problem. Double-check your src/lib.rs against the provided code.
  3. Reducer not callable or incorrect arguments:
    • Issue: You might have typos in the reducer name when calling it from spacetime console, or you’re providing the wrong number/type of arguments.
    • Solution: Ensure the call command exactly matches the reducer’s name and argument types. For Option<String>, remember to pass the string in quotes or None.

Summary

Congratulations! You’ve successfully completed your first SpaceTimeDB project. Here are the key takeaways from this chapter:

  • SpaceTimeDB CLI: Your essential tool for project management, compilation, and running your local instance.
  • Project Structure: A SpaceTimeDB project consists of src/lib.rs (your Rust logic) and Spacetime.toml (configuration).
  • Schema Definition: You define your database tables using Rust structs annotated with #[spacetimedb(table)] and #[primarykey]. This brings strong type safety directly to your database.
  • Reducers: These Rust functions, marked with #[spacetimedb(reducer)], are the only way to modify your SpaceTimeDB’s state. They provide a deterministic and controlled way to implement your application’s logic.
  • Local Development: The spacetime dev command compiles your code, starts a local server, and provides live reloading for a smooth development experience.
  • Console Interaction: The spacetime console allows you to directly call reducers and subscribe to tables, making it easy to test and debug your backend.

You now have a solid foundation for building real-time applications with SpaceTimeDB. In the next chapter, we’ll connect a simple web frontend to our my_todo_app and see how clients interact with our SpaceTimeDB instance to perform real-time data synchronization. Get ready to witness the magic!

References

  1. SpaceTimeDB Official Documentation: https://spacetimedb.com/docs
  2. Rust Programming Language: https://www.rust-lang.org/
  3. crates.io - spacetimedb-cli: https://crates.io/crates/spacetimedb-cli
  4. SpaceTimeDB GitHub Repository: https://github.com/clockworklabs/SpacetimeDB

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.