Introduction: The Blueprint for Your Real-time World
Welcome back, future SpaceTimeDB architects! In our previous chapters, we got acquainted with what SpaceTimeDB is and set up our development environment. Now, it’s time to lay the foundation for your real-time applications: designing your database schema.
Just as an architect draws up blueprints before construction begins, you’ll define your data’s structure and relationships within SpaceTimeDB. This chapter is crucial because a well-designed schema isn’t just about storing data; it’s about enabling efficient real-time synchronization, consistent state management, and robust server-side logic. We’ll explore how SpaceTimeDB combines the power of Rust with database table definitions to create a unified data model.
By the end of this chapter, you’ll understand how to define tables, specify primary keys and indexes, and logically relate different pieces of data. You’ll be ready to translate your application’s data requirements into a SpaceTimeDB module, setting the stage for building dynamic, collaborative experiences.
Core Concepts: Defining Your Data Universe
In SpaceTimeDB, your database schema is much more than just a list of tables and columns. It’s a Rust-based module that defines both your data’s structure and, as we’ll see in later chapters, your server-side logic (reducers). Let’s break down the core components.
What is a SpaceTimeDB Schema?
Think of your SpaceTimeDB schema as the master plan for your entire application’s backend. It’s written in Rust, a powerful and safe programming language, and then compiled into WebAssembly (WASM). This WASM module is what SpaceTimeDB runs internally to manage your database, execute your logic, and ensure deterministic, consistent state across all connected clients.
Why Rust and WebAssembly?
- Performance: Rust compiles to highly optimized native code, offering incredible speed. WASM provides a near-native performance environment.
- Safety & Determinism: Rust’s strong type system and ownership model prevent common programming errors, leading to more reliable and deterministic server-side logic. This determinism is key to SpaceTimeDB’s ability to maintain a consistent shared state.
- Unified Logic: By defining both data and logic in the same Rust module, SpaceTimeDB creates a tightly integrated system where your database schema and business rules live side-by-side, simplifying development and deployment.
Tables: The Building Blocks of Your Data
At its heart, SpaceTimeDB organizes data into tables, much like traditional relational databases. Each table represents a collection of similar items, like “Users,” “TodoItems,” or “GameCharacters.”
To define a table in SpaceTimeDB, you’ll use a standard Rust struct and adorn it with special attributes (called “derive macros” in Rust) provided by the SpacetimeDB library.
Let’s look at the key elements:
#[derive(SpacetimeTable)]: This macro is essential. It tells the SpaceTimeDB compiler that your Ruststructshould be treated as a database table. It automatically generates the necessary code to allow SpaceTimeDB to store, retrieve, and synchronize instances of this struct.- Fields and Data Types: Inside your struct, you define fields using standard Rust data types (
u64,String,bool, etc.) or even other custom structs (which can also be tables themselves or embedded data). - Primary Keys (
#[primarykey]): Every table must have a primary key. This field uniquely identifies each row in your table. It’s crucial for efficient data retrieval and ensuring data integrity. Primary keys in SpaceTimeDB are typicallyu64(unsigned 64-bit integers). - Indexes (
#[index]): While a primary key ensures unique identification, secondary indexes improve the performance of queries that filter or sort by other fields. If you frequently search for users by theirnameor items by theircategory, adding an#[index]to those fields will speed up those operations significantly. - Unique Constraints (
#[unique]): You can also mark a field with#[unique]to ensure that no two rows in the table have the same value for that specific field. This is often used for things like usernames or email addresses.
Here’s a conceptual view of how your Rust code maps to SpaceTimeDB tables:
Relations: Connecting Your Data
In SpaceTimeDB, you don’t define explicit FOREIGN KEY constraints in the same way you would in a traditional SQL database. Instead, relationships between tables are established logically by referencing the primary key of one table in another.
For example, if you have a User table and a TodoItem table, a TodoItem doesn’t “belong” to a User through a database constraint. Instead, the TodoItem struct would simply contain a field, say user_id, which stores the id of the User it’s associated with.
This approach offers flexibility and is very efficient for real-time synchronization. SpaceTimeDB’s client SDKs (which we’ll cover later) make it easy to “join” this data on the client side, allowing you to build rich UIs that display related information.
Why this approach?
- Flexibility: Avoids rigid database-level constraints that can sometimes complicate schema evolution or distributed systems.
- Performance: Lookup by primary key (ID) is extremely fast.
- Client-side Joins: SpaceTimeDB’s reactive nature means clients can efficiently subscribe to and combine data from multiple tables based on these logical links.
Step-by-Step Implementation: Building Our First Schema
Let’s put these concepts into practice by creating a simple “Todo List” application schema. Our application will need to store User information and TodoItems, with each TodoItem belonging to a specific User.
Prerequisites
Make sure you’ve completed the setup from Chapter 2 and have the spacetime CLI installed. We’ll assume you’ve already created a new SpaceTimeDB project (e.g., spacetime new todo-app) and are working within its directory.
If you haven’t, open your terminal and run:
spacetime new todo-app
cd todo-app
1. Open Your Schema File
Navigate to the src directory within your todo-app project. You’ll find a file named spacetime.rs. This is where your schema and server-side logic will live.
Open src/spacetime.rs in your favorite code editor. It might contain some boilerplate code. For now, let’s clear it out so we can start fresh.
Your src/spacetime.rs file should initially look something like this (you might remove any example structs if present):
#![allow(warnings)] // Allow warnings for now, good practice to remove later
use spacetimedb::{
spacetimedb,
SpacetimeTable,
};
// Your schema definitions and reducers will go here
2. Define the User Table
First, let’s define our User table. Each user will have a unique id and a name.
Add the following Rust code to your src/spacetime.rs file, just below the use statements:
#[spacetimedb(table)]
pub struct User {
#[primarykey]
pub id: u64,
pub name: String,
}
Let’s break down these lines:
#[spacetimedb(table)]: This is the attribute that marks ourUserstruct as a SpaceTimeDB table. It’s a newer, more concise syntax that replaces#[derive(SpacetimeTable)]in recentv2.xversions of SpaceTimeDB, aligning with modern Rust attribute usage.pub struct User { ... }: Defines a public Rust struct namedUser.pubmeans it’s publicly accessible.#[primarykey]: This attribute designates theidfield as the primary key for theUsertable. It must be unique for eachUserentry.pub id: u64: Theidfield is a public unsigned 64-bit integer, a common type for primary keys.pub name: String: Thenamefield is a public RustString, which will store the user’s name.
3. Define the TodoItem Table
Next, let’s define the TodoItem table. Each todo item will have its own unique id, a user_id to link it to a User, a description, and a completed status.
Add this code below your User struct definition in src/spacetime.rs:
#[spacetimedb(table)]
pub struct TodoItem {
#[primarykey]
pub id: u64,
#[index]
pub user_id: u64, // Logical link to a User's id
pub description: String,
pub completed: bool,
}
And here’s the explanation:
#[spacetimedb(table)]: Again, markingTodoItemas a SpaceTimeDB table.pub id: u64: The primary key for theTodoItem.#[index] pub user_id: u64: This field holds theidof theUserwho owns this todo item. We’ve added#[index]because we’ll likely want to quickly retrieve all todo items for a specific user. This index will make those lookups fast.pub description: String: The text content of the todo item.pub completed: bool: A boolean indicating whether the todo item has been completed.
Your complete src/spacetime.rs file should now look like this:
#![allow(warnings)] // Allow warnings for now, good practice to remove later
use spacetimedb::{
spacetimedb,
SpacetimeTable,
};
#[spacetimedb(table)]
pub struct User {
#[primarykey]
pub id: u64,
pub name: String,
}
#[spacetimedb(table)]
pub struct TodoItem {
#[primarykey]
pub id: u64,
#[index]
pub user_id: u64, // Logical link to a User's id
pub description: String,
pub completed: bool,
}
4. Compiling and Deploying Your Schema
Now that we’ve defined our schema, it’s time to compile our Rust code into a WebAssembly module and then deploy it to a SpaceTimeDB instance.
Build the Module: Open your terminal in the
todo-appproject directory and run:spacetime buildThis command invokes the Rust compiler to build your
src/spacetime.rsfile into a.wasmfile, typically located attarget/spacetime.wasm. You should see output indicating a successful build. If there are any Rust syntax errors, the compiler will tell you here!Deploy the Module: Once built, deploy it to your local SpaceTimeDB instance. We’ll use the
spacetime db startcommand, which both starts the database and deploys your module.spacetime db start --module-path target/spacetime.wasmYou should see output indicating that SpaceTimeDB is starting up and your module has been successfully deployed. This command effectively applies your schema to the running SpaceTimeDB instance.
Note on
spacetime db deployvsspacetime db start --module-path:spacetime db deployis used to deploy a new module to an already running SpaceTimeDB instance.spacetime db start --module-pathis convenient for local development, as it starts the database and deploys your module in one go. We’ll use this primarily for now.
5. Verifying Your Schema
How do we know our schema was deployed correctly? The SpaceTimeDB CLI provides ways to inspect the database.
While SpaceTimeDB v2.x focuses heavily on client SDKs for interaction, you can get a basic confirmation of table existence.
Connect to the CLI (if not already connected): If your
spacetime db startcommand is running in one terminal, open a second terminal in thetodo-appdirectory.List Tables (Conceptual): Currently, direct CLI commands for listing schema details are being continuously enhanced. The primary way to interact and observe your schema is through client SDKs (which we’ll cover in the next chapter) or by checking the logs of your
spacetime db startcommand for successful module deployment.For now, trust that if
spacetime buildandspacetime db start --module-pathcompleted without errors, your schema has been successfully applied! The SpaceTimeDB instance now understands theUserandTodoItemtables and their defined fields.In future chapters, when we connect a client, we’ll see exactly how to query and interact with these tables.
Mini-Challenge: Enhancing Your TodoItem
Let’s expand our TodoItem table with a couple more practical fields. This will solidify your understanding of adding fields and applying indexes.
Challenge:
- Add a
priorityfield to theTodoItemstruct. This field should be anu8(unsigned 8-bit integer), representing priority levels from 0 (low) to 255 (high). - Make the
priorityfield indexable, as you might want to quickly find all high-priority tasks. - Add an
created_atfield to theTodoItemstruct. This field should be au64, storing a Unix timestamp (milliseconds since epoch) indicating when the todo item was created. This field does not need an index for now.
Hint:
- Remember the
#[index]attribute for indexable fields. - After making changes to
src/spacetime.rs, you’ll need to runspacetime buildagain, and then restart your database withspacetime db start --module-path target/spacetime.wasmto apply the new schema.
Click for Solution (after you've tried it!)
#![allow(warnings)]
use spacetimedb::{
spacetimedb,
SpacetimeTable,
};
#[spacetimedb(table)]
pub struct User {
#[primarykey]
pub id: u64,
pub name: String,
}
#[spacetimedb(table)]
pub struct TodoItem {
#[primarykey]
pub id: u64,
#[index]
pub user_id: u64, // Logical link to a User's id
pub description: String,
pub completed: bool,
#[index] // Added index here
pub priority: u8, // New field for priority
pub created_at: u64, // New field for creation timestamp
}
After updating the file, run:
spacetime build
spacetime db start --module-path target/spacetime.wasm
What to observe/learn: You’ve now successfully modified your schema, added new data types, and applied another index. This demonstrates the iterative nature of schema design in SpaceTimeDB.
Common Pitfalls & Troubleshooting
Even with a strong type system like Rust’s, you might encounter a few common issues when designing your SpaceTimeDB schema.
- Rust Compilation Errors: The most frequent issue for beginners.
- Problem:
spacetime buildfails with Rust compiler errors. - Solution: Read the error messages carefully. Rust’s compiler is famously helpful! It often tells you exactly where the error is, what’s expected, and sometimes even suggests fixes. Common issues include missing semicolons, incorrect types, or uninitialized fields.
- Problem:
- Missing
#[spacetimedb(table)]or#[primarykey]:- Problem: Your module builds, but SpaceTimeDB doesn’t recognize your table, or deployment fails with a schema-related error.
- Solution: Double-check that every struct intended to be a table has
#[spacetimedb(table)]above it, and that exactly one field within each table struct is marked with#[primarykey].
- Incorrect Data Types:
- Problem: You try to store a
Stringin au64field, or vice-versa, which will be caught by the Rust compiler. - Solution: Ensure your Rust types match the kind of data you intend to store. SpaceTimeDB supports most primitive Rust types and certain complex types. Consult the official SpaceTimeDB documentation for a comprehensive list of supported types: https://spacetimedb.com/docs/
- Problem: You try to store a
- Forgetting
spacetime buildorspacetime db start:- Problem: You make changes to
spacetime.rsbut your database doesn’t reflect them, or your client code (in later chapters) can’t find the new fields/tables. - Solution: Always remember the two-step process:
spacetime buildto compile your Rust code, thenspacetime db start --module-path target/spacetime.wasm(orspacetime db deploy) to apply the compiled module to the running database instance.
- Problem: You make changes to
Summary: Your Data Takes Shape
Congratulations! You’ve successfully designed and deployed your first SpaceTimeDB schema. Let’s recap the key takeaways from this chapter:
- Schema as Blueprint: Your SpaceTimeDB schema, written in Rust, defines both your data’s structure and your server-side logic, compiled into a WebAssembly module.
- Tables from Structs: Rust
structs decorated with#[spacetimedb(table)]become your database tables. - Primary Keys: Every table must have a
#[primarykey]field (typicallyu64) for unique identification. - Indexes for Speed: Use
#[index]on fields you’ll frequently query or filter by to ensure performant lookups.#[unique]ensures field values are distinct across rows. - Logical Relations: Relationships between tables are established by referencing primary keys (e.g.,
user_idinTodoItemlinking toUser.id), rather than explicit foreign key constraints. - Build and Deploy: The workflow involves
spacetime buildto compile your Rust module andspacetime db start --module-path target/spacetime.wasm(orspacetime db deploy) to apply it to your SpaceTimeDB instance.
You now have a solid understanding of how to structure your application’s data within SpaceTimeDB. In the next chapter, we’ll learn how to interact with this schema by writing server-side logic using SpaceTimeDB’s “reducers” to create, update, and delete data, bringing your real-time application to life!
References
- SpacetimeDB Official Documentation
- SpacetimeDB GitHub Repository
- Rust Programming Language Documentation
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.