Welcome back, intrepid developer! In our previous chapters, we’ve explored the foundational concepts of SpaceTimeDB, from setting up your development environment to designing schemas and writing server-side logic using reducers. We’ve seen how SpaceTimeDB acts as a unified backend, combining a database with application logic.
Now, it’s time to unveil the “magic” that makes SpaceTimeDB truly shine: its real-time capabilities. This chapter will pull back the curtain on how client applications stay perfectly synchronized with your SpaceTimeDB instance, receiving instant updates as data changes. We’ll explore the core mechanisms of client synchronization, event propagation, and how to build responsive, collaborative experiences.
By the end of this chapter, you’ll understand:
- How SpaceTimeDB clients establish and maintain real-time connections.
- The power of “subscriptions” to declaratively express interest in data.
- How SpaceTimeDB efficiently propagates changes and events to connected clients.
- How to integrate a basic SpaceTimeDB client into a web application.
- The fundamental patterns for building real-time features like live dashboards or multiplayer game elements.
Ready to make your applications come alive with real-time updates? Let’s dive in!
Core Concepts: The Pulse of Real-time
Traditional web applications often rely on a “request-response” model. A client asks for data, the server responds, and then the connection closes. To get updates, the client has to ask again (polling) or use complex, separate technologies like WebSockets and pub/sub systems. SpaceTimeDB elegantly solves this by integrating real-time synchronization directly into its core.
Client-Server Communication: Always Connected
At the heart of SpaceTimeDB’s real-time capabilities is a persistent, bidirectional communication channel between the client and the SpaceTimeDB instance. This channel is typically established using WebSockets.
When a client connects, it doesn’t just make a one-off request; it opens a continuous pipeline. This pipeline allows the client to:
- Send requests: Call reducers, execute queries.
- Receive updates: Get notified instantly when data it cares about changes on the server.
Think of it like a phone call versus sending a letter. With a phone call, once connected, you can have a continuous conversation, sending and receiving information in real-time.
The Power of Subscriptions
How does SpaceTimeDB know what data a client “cares about”? This is where subscriptions come into play. A subscription is a declarative instruction from the client to SpaceTimeDB, saying, “Hey, I’m interested in all data from TableA,” or “I want to know about all users with status = 'online'.”
Once subscribed, SpaceTimeDB continuously monitors the requested data. Any time a relevant change occurs (an insert, update, or delete), SpaceTimeDB automatically pushes that change to the subscribing client.
This is a stark contrast to traditional database queries, which are “point-in-time” snapshots. A subscription is a “live query” that continuously updates its results.
Why Subscriptions are Smart:
- Efficiency: Clients only receive data they explicitly ask for, reducing network traffic.
- Simplicity: Developers don’t need to write complex polling logic or manage manual WebSocket messages. SpaceTimeDB handles the “when to send” and “what to send.”
- Consistency: All subscribed clients see the same, consistent state as it evolves, thanks to SpaceTimeDB’s deterministic nature.
How SpaceTimeDB Propagates Events
When a reducer executes and modifies the database, SpaceTimeDB doesn’t just update its internal state; it also generates a stream of events. These events describe what changed (e.g., “row inserted into users table,” “value updated in messages table”).
SpaceTimeDB then intelligently compares these changes against all active client subscriptions. If a change affects data that a client is subscribed to, SpaceTimeDB serializes the relevant event and pushes it down the client’s WebSocket connection.
This process is incredibly fast and efficient, designed to deliver updates with minimal latency. It’s the engine that powers real-time collaboration and dynamic user interfaces.
Deterministic State and Event Sourcing
A key architectural principle of SpaceTimeDB (which we touched upon in previous chapters) is its foundation in deterministic event sourcing. Every change to the database is a result of a reducer executing based on an incoming client call. These reducer calls are logged as a sequence of events.
Because reducers are deterministic, applying the same sequence of events to an initial state will always result in the same final state. This property is crucial for:
- Consistency: Ensuring all clients, when caught up, see the identical database state.
- Replication: Easily replicating the database by replaying event logs.
- Debugging: Understanding exactly how a state was reached.
When clients connect, they often receive an initial “snapshot” of the subscribed data and then continuously receive new events to keep their local view up-to-date.
Visualizing the Real-time Flow
Let’s look at a simplified diagram of how a client interaction leads to real-time updates for other clients:
Explanation of the flow:
- Client A Calls Reducer: Client A interacts with the application, which triggers a call to a SpaceTimeDB reducer (e.g.,
create_user). - SpaceTimeDB Executes Reducer: The SpaceTimeDB instance receives the call and executes the
create_userreducer. - State Change: The reducer modifies the SpaceTimeDB database, changing its state.
- Generates Events: SpaceTimeDB internally records this state change as an event (e.g., a new user row was inserted).
- Checks Subscriptions: SpaceTimeDB then checks which connected clients have subscriptions that would be affected by this new event.
- Propagates Events: For every affected client (including Client A, Client B, Client C in this example), SpaceTimeDB pushes the relevant event (the new user data) down their WebSocket connection. All clients see the update in real-time!
Step-by-Step Implementation: Connecting a Web Client
Let’s put these concepts into practice. We’ll set up a simple HTML page with JavaScript to connect to our SpaceTimeDB instance, subscribe to a table, and react to real-time updates.
Prerequisites:
- You have SpaceTimeDB CLI installed and a basic project initialized (from Chapter 2).
- You have a SpaceTimeDB module running with at least one table and a reducer. For this example, let’s assume we have a
userstable and acreate_userreducer, similar to what we might have built in Chapter 4.
Example users table schema (from Chapter 4, for reference):
// In your module's lib.rs or a separate schema file
#[spacetimedb(table)]
pub struct User {
#[primarykey]
#[autoinc]
pub id: u64,
pub username: String,
pub created_at: u64,
}
#[spacetimedb(reducer)]
pub fn create_user(ctx: ReducerContext, username: String) -> Result<(), String> {
if username.is_empty() {
return Err("Username cannot be empty".to_string());
}
let now = ctx.timestamp;
User::insert(User { id: 0, username, created_at: now });
Ok(())
}
Make sure your SpaceTimeDB instance is running:
spacetime start
spacetime deploy
This will typically run on ws://localhost:3000 by default.
Step 1: Create a Simple HTML File
Let’s create an index.html file that will host our JavaScript client.
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SpaceTimeDB Real-time Users</title>
<style>
body { font-family: sans-serif; margin: 20px; }
#user-list { border: 1px solid #ccc; padding: 10px; min-height: 100px; margin-top: 20px; }
.user-item { margin-bottom: 5px; padding: 5px; background-color: #f9f9f9; border-radius: 3px; }
input[type="text"] { padding: 8px; margin-right: 10px; border: 1px solid #ddd; }
button { padding: 8px 15px; background-color: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; }
button:hover { background-color: #0056b3; }
</style>
</head>
<body>
<h1>SpaceTimeDB User List</h1>
<div>
<input type="text" id="username-input" placeholder="Enter new username">
<button id="add-user-button">Add User</button>
</div>
<h2>Current Users:</h2>
<div id="user-list">
<!-- Users will appear here in real-time -->
<p>Connecting to SpaceTimeDB...</p>
</div>
<!-- Include the SpacetimeDB Client Library -->
<script src="https://unpkg.com/@clockworklabs/[email protected]/dist/index.js"></script>
<script src="app.js"></script>
</body>
</html>
Explanation:
- We’re including the
spacetimedb-clientlibrary directly from unpkg. This is convenient for quick examples. For production, you’d typically install it via npm (npm install @clockworklabs/spacetimedb-client) and bundle it with a tool like Webpack or Vite. We’re usingv2.0.0as of 2026-03-14, which is the latest stable major release. - We have a simple input and button to add users, and a
divto display the list of users. - The
app.jsfile is where our SpaceTimeDB client logic will reside.
Step 2: Write the JavaScript Client Logic
Now, create public/app.js and add the following code step-by-step.
Part A: Connect to SpaceTimeDB
// public/app.js
// 1. Import the SpacetimeDBClient
// If using module bundler: import { SpacetimeDBClient } from '@clockworklabs/spacetimedb-client';
// Since we're using a script tag, it's globally available as SpacetimeDBClient.
const SPACETIMEDB_URI = 'ws://localhost:3000'; // Your SpaceTimeDB instance URI
const DB_NAME = 'your_project_name'; // Replace with your actual project name from `spacetime.toml`
// Create a new client instance
const client = new SpacetimeDBClient(SPACETIMEDB_URI, DB_NAME);
// Get references to DOM elements
const userListDiv = document.getElementById('user-list');
const usernameInput = document.getElementById('username-input');
const addUserButton = document.getElementById('add-user-button');
// Store users locally
let users = {}; // Object to store users by ID for easy lookup and update
// 2. Handle connection status
client.onConnect(() => {
console.log('Connected to SpaceTimeDB!');
userListDiv.innerHTML = '<p>Connected. Subscribing to users...</p>';
// We'll add subscription logic here next
});
client.onDisconnect(() => {
console.log('Disconnected from SpaceTimeDB.');
userListDiv.innerHTML = '<p>Disconnected from SpaceTimeDB. Please refresh.</p>';
});
client.onReconnect(() => {
console.log('Reconnected to SpaceTimeDB.');
userListDiv.innerHTML = '<p>Reconnected. Fetching latest data...</p>';
});
client.onError((error) => {
console.error('SpaceTimeDB Client Error:', error);
userListDiv.innerHTML = `<p style="color:red;">Error: ${error.message}. Check console for details.</p>`;
});
// 3. Connect to the database
client.connect();
Explanation:
- We initialize
SpacetimeDBClientwith the WebSocket URI and your project’s database name (found inspacetime.toml). Make sure to replaceyour_project_name. - We set up event listeners for
onConnect,onDisconnect,onReconnect, andonErrorto provide feedback on the connection status. - Finally,
client.connect()initiates the WebSocket connection.
Part B: Subscribe to the users Table
Now, let’s add the subscription logic to app.js inside the onConnect callback.
// ... (previous code) ...
// Store users locally
let users = {}; // Object to store users by ID for easy lookup and update
// Function to render the user list
function renderUserList() {
userListDiv.innerHTML = ''; // Clear existing list
const userArray = Object.values(users).sort((a, b) => a.created_at - b.created_at); // Sort by creation time
if (userArray.length === 0) {
userListDiv.innerHTML = '<p>No users yet. Add one!</p>';
return;
}
userArray.forEach(user => {
const userItem = document.createElement('div');
userItem.className = 'user-item';
userItem.textContent = `ID: ${user.id}, Username: ${user.username}, Created: ${new Date(Number(user.created_at)).toLocaleString()}`;
userListDiv.appendChild(userItem);
});
}
client.onConnect(() => {
console.log('Connected to SpaceTimeDB!');
userListDiv.innerHTML = '<p>Connected. Subscribing to users...</p>';
// 4. Subscribe to the 'users' table
// The subscribe method takes the table name as a string.
client.subscribe(['User']); // 'User' is the name of our table in Rust
// 5. Listen for table updates
// These events fire whenever a row is inserted, updated, or deleted in the 'User' table
client.on('User:onInsert', (userRow, reducerCtx) => {
console.log('User inserted:', userRow);
users[userRow.id] = userRow; // Add to local state
renderUserList();
});
client.on('User:onUpdate', (oldUserRow, newUserRow, reducerCtx) => {
console.log('User updated:', oldUserRow, '->', newUserRow);
users[newUserRow.id] = newUserRow; // Update local state
renderUserList();
});
client.on('User:onDelete', (userRow, reducerCtx) => {
console.log('User deleted:', userRow);
delete users[userRow.id]; // Remove from local state
renderUserList();
});
// 6. Initial data load: After subscription, the client will receive all existing data.
// The 'User:onInitialQueryResult' event fires once with all current rows.
client.on('User:onInitialQueryResult', (initialUsers) => {
console.log('Initial user query result:', initialUsers);
users = initialUsers.reduce((acc, user) => {
acc[user.id] = user;
return acc;
}, {});
renderUserList();
});
});
// ... (rest of the code) ...
Explanation:
client.subscribe(['User']): This is the crucial line! It tells SpaceTimeDB we want all data from theUsertable. Note that the table name matches the Rust struct name.client.on('User:onInsert', ...): This event listener fires whenever a newUserrow is inserted into the database by a reducer.client.on('User:onUpdate', ...): This listener fires when an existingUserrow is modified.client.on('User:onDelete', ...): This listener fires when aUserrow is removed.client.on('User:onInitialQueryResult', ...): When you first subscribe, SpaceTimeDB sends all existing data for that table. This event handles that initial batch. We populate our localusersobject and then render.renderUserList(): A helper function to take ourusersobject and display it in the HTML.
Part C: Call a Reducer from the Client
Finally, let’s add the logic to call our create_user reducer when the “Add User” button is clicked.
// ... (previous code including onConnect, onInsert, etc.) ...
// 7. Call the create_user reducer when the button is clicked
addUserButton.addEventListener('click', async () => {
const username = usernameInput.value.trim();
if (username) {
try {
console.log(`Calling create_user reducer with username: ${username}`);
// The callReducer method takes the reducer name and its arguments
await client.callReducer('create_user', username);
usernameInput.value = ''; // Clear input after successful call
} catch (error) {
console.error('Error calling create_user reducer:', error);
alert(`Failed to add user: ${error.message}`);
}
} else {
alert('Please enter a username!');
}
});
// 8. Connect to the database
client.connect();
Explanation:
- We add an event listener to the
addUserButton. - Inside the listener,
client.callReducer('create_user', username)sends a request to SpaceTimeDB to execute ourcreate_userreducer with the providedusername. - Since the reducer modifies the
Usertable, SpaceTimeDB will automatically detect this change and push anonInsertevent to all subscribed clients, including our own. This will trigger ouronInsertlistener and update the UI in real-time!
Step 3: Test Your Real-time Application
- Ensure SpaceTimeDB is running:Verify it’s running on
spacetime start spacetime deploylocalhost:3000. - Open
public/index.htmlin your browser. You can simply drag and drop the file into your browser, or serve it with a simple local web server (e.g., Python’shttp.serverorlive-servernpm package).# From your project root, assuming public/index.html and public/app.js cd public python3 -m http.server 8000 # Then open http://localhost:8000 in your browser - Observe:
- You should see “Connected to SpaceTimeDB…” and then “No users yet. Add one!”
- Open your browser’s developer console (F12) to see the
console.logmessages. - Type a username into the input field and click “Add User”.
- The user should appear instantly in the “Current Users” list!
- Crucially, open a second browser tab or window to
http://localhost:8000. When you add a user in one tab, it should appear instantly in the other tab as well, without refreshing! This is the real-time magic in action.
Mini-Challenge: Enhance User Display
You’ve seen the basic real-time update. Now, let’s make it a bit more interactive.
Challenge:
Modify the renderUserList function and the user-item styling so that:
- Each user item includes a “Delete” button.
- Clicking the “Delete” button calls a new SpaceTimeDB reducer named
delete_userthat takes auser_idand removes the corresponding user from theUsertable. - Observe that deleting a user in one browser tab instantly removes it from all other connected tabs.
Hint:
- You’ll need to define a new
delete_userreducer in your Rust module (lib.rs). It should takeuser_id: u64and useUser::delete(user_id)to remove the row. Remember to handle potential errors (e.g., user not found). - In
renderUserList, create the button element, attach anonclickevent listener that callsclient.callReducer('delete_user', userId), and append it to theuserItem. - Remember to handle the
User:onDeleteevent in your JavaScript client to update theusersobject and re-render.
Common Pitfalls & Troubleshooting
- Incorrect SpaceTimeDB URI or DB Name:
- Symptom: Client shows “Disconnected” or “Error: WebSocket connection failed.”
- Fix: Double-check
SPACETIMEDB_URI(defaultws://localhost:3000) andDB_NAMEinapp.js. TheDB_NAMEmust match thenamefield in yourspacetime.tomlfile.
- Table Name Mismatch:
- Symptom: Client connects but doesn’t receive any data or updates for subscribed tables.
- Fix: Ensure the table name passed to
client.subscribe(['TableName'])exactly matches the Rust struct name defined with#[spacetimedb(table)](e.g.,Usernotusers). Case sensitivity matters!
- Reducer Logic Errors:
- Symptom: Reducer calls complete without error, but no data appears, or incorrect data appears.
- Fix: Check your Rust reducer logic (
lib.rs). Does it correctlyinsert,update, ordeletethe intended data? Useconsole.logon the client side andspacetime logon the server side to trace reducer execution and database changes.
- No
client.connect():- Symptom: Nothing happens, no connection attempts.
- Fix: Ensure
client.connect()is called at the end of yourapp.jsfile.
- Running Client Before SpaceTimeDB:
- Symptom: WebSocket connection errors.
- Fix: Always ensure your SpaceTimeDB instance is running (
spacetime startandspacetime deploy) before opening your client application.
Summary
Congratulations! You’ve just built your first truly real-time application using SpaceTimeDB. You’ve seen how SpaceTimeDB seamlessly integrates database management, backend logic, and real-time synchronization into a single, powerful platform.
Here are the key takeaways from this chapter:
- SpaceTimeDB uses WebSockets for persistent, bidirectional client-server communication.
- Subscriptions are the declarative way for clients to express interest in specific data, enabling SpaceTimeDB to push relevant updates.
- SpaceTimeDB generates events for every database change, which are then propagated to subscribed clients.
- The deterministic event-sourced architecture ensures consistency across all connected clients.
- The
@clockworklabs/spacetimedb-clientlibrary provides a straightforward API for connecting, subscribing, and calling reducers from web applications.
You’re now equipped to start building dynamic, collaborative, and highly responsive applications that leverage SpaceTimeDB’s real-time capabilities.
What’s Next? In the next chapter, we’ll dive deeper into more advanced subscription patterns, including how to filter and query data efficiently on the client side, and explore how to structure more complex multiplayer or collaborative application patterns.
References
- SpacetimeDB Official Documentation
- SpacetimeDB GitHub Repository
- SpacetimeDB Client Library on unpkg
- MDN Web Docs - WebSockets
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.