Introduction
Welcome to Chapter 8! So far, we’ve explored the fascinating world of SpaceTimeDB, understanding its core concepts, how to define schemas, and how to implement server-side logic using reducers. We’ve built the “brain” of our real-time applications, where data lives and logic executes deterministically.
But what’s a powerful backend without a beautiful and interactive frontend? This chapter is all about bridging that gap. We’ll dive deep into how your client applications—whether they’re web apps built with JavaScript/TypeScript or games developed with engines like Unity using C#—connect to SpaceTimeDB, subscribe to real-time data updates, and invoke your server-side reducers. By the end of this chapter, you’ll be able to bring your SpaceTimeDB-powered ideas to life with dynamic, real-time user interfaces.
This chapter assumes you have a basic understanding of frontend development (HTML, CSS, JavaScript for web, or C# for Unity) and have a SpaceTimeDB instance running with some basic schema and reducers from previous chapters. Let’s make our applications truly interactive!
Core Concepts: Connecting Your Frontend to the Real-time Backend
Integrating a frontend with SpaceTimeDB is a seamless experience designed for real-time interaction. It leverages WebSockets for persistent connections and dedicated client SDKs to simplify the process.
The Role of Client SDKs
SpaceTimeDB provides client-side Software Development Kits (SDKs) for various languages and platforms. These SDKs are your primary interface for interacting with your SpaceTimeDB instance. They handle:
- WebSocket Connection Management: Establishing and maintaining a persistent WebSocket connection to your SpaceTimeDB server.
- Serialization/Deserialization: Converting your client-side data into a format SpaceTimeDB understands, and vice-versa.
- Real-time Event Handling: Listening for and processing database changes pushed from the server.
- Reducer Invocation: Providing a convenient way to call your server-side reducers from the client.
- Local Cache Management (Optional): Some SDKs might offer mechanisms for maintaining a local, consistent view of subscribed data, reducing the need for you to build this from scratch.
For web applications, the primary SDK is the JavaScript/TypeScript SDK. For game engines like Unity, a C# SDK is available.
Real-time Synchronization: How Data Flows
The magic of SpaceTimeDB lies in its real-time synchronization. When a client connects and subscribes to a table, here’s the general flow:
- Initial Sync: Upon successful connection and subscription, the client receives the current state of all subscribed tables. This is often referred to as the “initial sync.”
- Event Stream: After the initial sync, the client continuously receives a stream of “update” events whenever data in the subscribed tables changes on the server. These updates are granular, often indicating which rows were inserted, updated, or deleted.
- Client-Side Update: Your frontend application listens for these events and updates its UI or internal state accordingly, reflecting the latest changes in real-time.
This push-based model, powered by WebSockets, eliminates the need for manual polling, making your applications highly responsive and efficient.
Calling Reducers from the Client
Clients don’t directly modify the database. Instead, they invoke reducers defined on the SpaceTimeDB server. This is a critical security and consistency feature:
- Centralized Logic: All state-modifying logic resides on the server, ensuring all clients operate under the same rules.
- Deterministic Execution: Reducers execute deterministically, guaranteeing consistency across all replicas and clients.
- Security: Access control and validation logic can be enforced within reducers, preventing unauthorized or invalid operations.
When a client calls a reducer, the SDK sends the reducer’s name and its arguments to the SpaceTimeDB server. The server executes the reducer, and if successful, propagates the resulting database changes back to all subscribed clients.
Architecture Diagram: Client-Server Interaction
Let’s visualize this interaction:
- Client Application: This is your web browser or game client. It interacts with the SDK.
- SpaceTimeDB SDK: The client-side library that manages the WebSocket connection and handles communication.
- WebSocket Connection: The persistent, bidirectional link between the client and the server.
- Reducer Invocation: When the client wants to perform an action (like sending a message), it calls a reducer via the SDK.
- Reducers: Server-side functions that contain your business logic and modify the database.
- Database State: The core data managed by SpaceTimeDB.
- Propagate Changes: After a reducer modifies the database, SpaceTimeDB intelligently identifies the changes.
- Push Updates: These changes are then pushed in real-time over the WebSocket connection to all subscribed clients.
- Real-time Data Updates: The client SDK receives these updates, allowing the UI to react instantly.
Client-Side State Management
While SpaceTimeDB handles the source of truth and real-time propagation, your frontend still needs its own strategy for managing its local state. This could be:
- Direct DOM Manipulation (Vanilla JS): Simple for small examples.
- Framework-Specific State (React Hooks, Vuex, Angular Services): Integrating SpaceTimeDB updates into your chosen framework’s reactive system.
- Game Engine State (Unity MonoBehaviours): Updating game objects and components based on incoming data.
The key is to connect SpaceTimeDB’s update events to your frontend’s state management system so that your UI or game world reflects the latest shared state.
Step-by-Step Implementation: A Simple Web Chat Client
Let’s build a basic web chat application to demonstrate how to integrate SpaceTimeDB. We’ll assume you have a SpaceTimeDB project set up from previous chapters with a Message table and a create_message reducer.
Prerequisites:
- A running SpaceTimeDB instance (e.g., on
localhost:3000). - A SpaceTimeDB module with a
Messagetable and acreate_messagereducer.lib.stdb(Schema):// Define a Message table #[derive(SpacetimeDBType, Clone, PartialEq, Eq, Debug)] pub struct Message { #[primarykey] #[autoinc] pub id: u64, pub sender: String, pub text: String, pub timestamp: u64, // Unix timestamp }reducers.stdb(Reducer):use super::message::Message; #[reducer] pub fn create_message(sender: String, text: String) { let timestamp = spacetime::now(); // Get current timestamp from SpaceTimeDB Message::insert(Message { id: 0, // autoinc will assign a real ID sender, text, timestamp, }).unwrap(); }- Ensure your SpaceTimeDB module is compiled and running (e.g.,
spacetime db dev).
Step 1: Set up a Basic Web Project
Create a new folder named chat-frontend. Inside it, create index.html and script.js.
chat-frontend/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 Chat</title>
<style>
body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; }
#chat-container {
max-width: 600px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#messages {
border: 1px solid #ddd;
height: 300px;
overflow-y: scroll;
padding: 10px;
margin-bottom: 15px;
border-radius: 4px;
background-color: #e9e9e9;
}
.message {
margin-bottom: 8px;
line-height: 1.4;
}
.message strong { color: #333; }
.message span { color: #666; font-size: 0.9em; }
#message-form { display: flex; gap: 10px; }
#username-input, #message-input {
flex-grow: 1;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
#send-button {
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
#send-button:hover { background-color: #0056b3; }
#connection-status {
text-align: center;
margin-bottom: 10px;
font-size: 0.9em;
color: #555;
}
.status-connected { color: green; }
.status-disconnected { color: red; }
</style>
</head>
<body>
<div id="chat-container">
<h1>SpaceTimeDB Real-time Chat</h1>
<div id="connection-status">Connecting...</div>
<div id="messages">
<!-- Messages will be rendered here -->
</div>
<form id="message-form">
<input type="text" id="username-input" placeholder="Your Name" required>
<input type="text" id="message-input" placeholder="Type a message..." required>
<button type="submit" id="send-button">Send</button>
</form>
</div>
<script type="module" src="script.js"></script>
</body>
</html>
This is a standard HTML file with some basic styling and elements for our chat interface: a message display area, an input for username, an input for the message, and a send button. The <script type="module" src="script.js"></script> line is important as it allows us to use ES module syntax in script.js.
Step 2: Initialize Node.js Project and Install SpaceTimeDB SDK
Open your terminal in the chat-frontend directory.
Initialize Node.js project:
npm init -yThis creates a
package.jsonfile.Install SpaceTimeDB SDK:
npm install @clockworklabs/[email protected] --save-exactWe’re explicitly installing
v2.0.1as of 2026-03-14, which is a recent stable release for the SDK. The--save-exactflag ensures this specific version is recorded.Why this package? The
@clockworklabs/spacetimedb-sdkis the official JavaScript/TypeScript client library for SpaceTimeDB, abstracting away the WebSocket communication and providing convenient methods for interacting with your module.
Step 3: Connect to SpaceTimeDB and Subscribe to Messages
Now, let’s write the JavaScript code to connect and receive messages.
chat-frontend/script.js:
Start by importing the SDK and setting up basic DOM references.
// chat-frontend/script.js
import { SpacetimeDBClient, Identity } from '@clockworklabs/spacetimedb-sdk';
// --- DOM Element References ---
const messagesDiv = document.getElementById('messages');
const messageForm = document.getElementById('message-form');
const usernameInput = document.getElementById('username-input');
const messageInput = document.getElementById('message-input');
const connectionStatusDiv = document.getElementById('connection-status');
// --- Configuration ---
const SPACETIMEDB_HOST = 'localhost';
const SPACETIMEDB_PORT = 3000;
const SPACETIMEDB_DB_NAME = 'chat_module'; // Replace with your module's name
// --- Utility Functions ---
function appendMessage(sender, text, timestamp) {
const messageElement = document.createElement('div');
messageElement.classList.add('message');
const date = new Date(Number(timestamp) * 1000); // SpaceTimeDB timestamp is in seconds
messageElement.innerHTML = `<strong>${sender}</strong>: ${text} <span>(${date.toLocaleTimeString()})</span>`;
messagesDiv.appendChild(messageElement);
messagesDiv.scrollTop = messagesDiv.scrollHeight; // Scroll to bottom
}
function updateConnectionStatus(isConnected) {
if (isConnected) {
connectionStatusDiv.textContent = 'Connected to SpaceTimeDB';
connectionStatusDiv.className = 'status-connected';
} else {
connectionStatusDiv.textContent = 'Disconnected from SpaceTimeDB';
connectionStatusDiv.className = 'status-disconnected';
}
}
// --- Main Connection Logic ---
async function initializeSpacetimeDB() {
console.log(`Attempting to connect to SpaceTimeDB at ws://${SPACETIMEDB_HOST}:${SPACETIMEDB_PORT}`);
// Step 1: Connect to the SpaceTimeDB server
try {
await SpacetimeDBClient.connect(
SPACETIMEDB_HOST,
SPACETIMEDB_PORT,
SPACETIMEDB_DB_NAME,
// You can provide an existing Identity or let the SDK create one
Identity.fromHexString(localStorage.getItem('spacetime_identity') || undefined)
);
console.log('Connected to SpaceTimeDB!');
updateConnectionStatus(true);
// Store identity for persistence (optional, but good for user recognition)
localStorage.setItem('spacetime_identity', SpacetimeDBClient.identity.toHexString());
} catch (error) {
console.error('Failed to connect to SpaceTimeDB:', error);
updateConnectionStatus(false);
// Implement retry logic or alert user
return;
}
// Step 2: Subscribe to the 'Message' table
// The SDK will automatically handle initial sync and subsequent updates
SpacetimeDBClient.subscribe([
{ tableName: 'Message' }
]);
console.log('Subscribed to "Message" table.');
// Step 3: Handle initial data and real-time updates
// The `on` method allows us to listen to various events from the SDK.
// `initial_sync` fires when the client first connects and receives all subscribed data.
SpacetimeDBClient.on("initial_sync", () => {
console.log("Initial sync received!");
// Get all messages after initial sync and render them
const messages = SpacetimeDBClient.getEntities('Message');
messages.sort((a, b) => Number(a.timestamp) - Number(b.timestamp)); // Sort by timestamp
messages.forEach(msg => appendMessage(msg.sender, msg.text, msg.timestamp));
});
// `update` fires for every change (insert, update, delete) to any subscribed table
SpacetimeDBClient.on("update", ({ events }) => {
// We iterate through events to find changes to our 'Message' table
events.forEach(event => {
if (event.table_name === 'Message' && event.event_type === 'insert') {
const newMessage = event.row_new;
// row_new contains the new state of the inserted row
appendMessage(newMessage.sender, newMessage.text, newMessage.timestamp);
}
// You could also handle 'update' or 'delete' events here if needed
});
});
// Handle disconnection
SpacetimeDBClient.on("disconnect", () => {
console.warn("Disconnected from SpaceTimeDB. Attempting to reconnect...");
updateConnectionStatus(false);
// You might want to implement a more robust reconnection strategy here
setTimeout(initializeSpacetimeDB, 5000); // Try to reconnect after 5 seconds
});
// Step 4: Handle form submission to send messages (call reducer)
messageForm.addEventListener('submit', async (event) => {
event.preventDefault(); // Prevent default form submission
const sender = usernameInput.value.trim();
const text = messageInput.value.trim();
if (sender && text) {
console.log(`Calling reducer 'create_message' with sender: ${sender}, text: ${text}`);
try {
// Call the server-side reducer
await SpacetimeDBClient.call('create_message', sender, text);
messageInput.value = ''; // Clear message input after sending
} catch (error) {
console.error('Error calling create_message reducer:', error);
alert('Failed to send message. See console for details.');
}
} else {
alert('Please enter your name and a message.');
}
});
// Set a default username if not already set
if (!usernameInput.value) {
usernameInput.value = `Guest${Math.floor(Math.random() * 1000)}`;
}
}
// Kick off the connection process when the script loads
initializeSpacetimeDB();
Explanation of script.js:
import { SpacetimeDBClient, Identity } from '@clockworklabs/spacetimedb-sdk';: We import the necessary classes from the SDK.SpacetimeDBClientis our main entry point, andIdentityis used for client identification.- Configuration:
SPACETIMEDB_HOST,SPACETIMEDB_PORT, andSPACETIMEDB_DB_NAMEmust match your running SpaceTimeDB instance. SpacetimeDBClient.connect(...): This is the first crucial step. It establishes the WebSocket connection.Identity.fromHexString(localStorage.getItem('spacetime_identity') || undefined): This line attempts to retrieve a previously saved client identity fromlocalStorage. If found, the client will reconnect with the same identity, allowing SpaceTimeDB to recognize it. If not, a new identity is generated. This is great for persistent user sessions without full authentication (yet!).
SpacetimeDBClient.subscribe([{ tableName: 'Message' }]): Once connected, we tell SpaceTimeDB which tables we are interested in. Here, we subscribe to theMessagetable. This means the client will receive the initial data for this table and all subsequent changes.SpacetimeDBClient.on("initial_sync", ...): This event listener fires once when the client first receives the complete initial state of all subscribed tables. We useSpacetimeDBClient.getEntities('Message')to fetch all current messages and render them.SpacetimeDBClient.on("update", ...): This is where the real-time magic happens! This event fires every time there’s a change in any subscribed table. Theeventsarray contains detailed information about what changed (e.g.,event_type: 'insert',table_name: 'Message',row_newwith the inserted data). We filter for newMessageinserts and append them to our chat display.SpacetimeDBClient.on("disconnect", ...): A simple handler for when the connection drops, attempting to reconnect after a delay.- Form Submission (
messageForm.addEventListener):- When the form is submitted, we prevent the default browser behavior.
SpacetimeDBClient.call('create_message', sender, text): This is how we invoke our server-side reducer. The first argument is the reducer’s name (as defined inreducers.stdb), and subsequent arguments are the parameters it expects, in order. The SDK handles sending this over the WebSocket and waiting for confirmation.- After a successful call, we clear the message input.
Step 4: Serve the Frontend
To run this, you need a simple web server. You can use Python’s built-in server or Node.js http-server.
Using Node.js http-server:
- Install
http-server(if you don’t have it):npm install -g http-server - Run the server from your
chat-frontenddirectory:This will serve yourhttp-server .index.htmlonhttp://localhost:8080(or another port).
Step 5: Test Your Real-time Chat
- Ensure your SpaceTimeDB instance is running (e.g.,
spacetime db dev). - Open your browser to
http://localhost:8080. - Open a second browser tab or window to the same address.
- Type messages in either window. You should see them appear in real-time in both windows!
Congratulations! You’ve successfully built your first real-time web application integrated with SpaceTimeDB.
Integration with Game Engines (Conceptual)
While a full Unity example is beyond the scope of a single chapter, the principles are identical for game engines using the C# SDK.
- Install C# SDK: Add the SpaceTimeDB C# SDK package to your Unity project. This is typically done via a
.unitypackageor NuGet. - Connect: In a MonoBehaviour script (e.g.,
GameManager.cs), useSpacetimeDBClient.ConnectAsync()to establish the connection to your SpaceTimeDB server. - Subscribe: Call
SpacetimeDBClient.Subscribe()to specify which tables your game needs to observe (e.g.,Player,Enemy,Projectile). - Handle Updates:
- Register callbacks for
SpacetimeDBClient.onUpdateor specific table change events. - When an update arrives, use the data to update your game’s entities: move player characters, spawn new objects, update health bars, etc.
- Remember to handle updates on the main Unity thread if you’re modifying UI or game objects, as SpaceTimeDB events might arrive on a background thread.
- Register callbacks for
- Call Reducers: When a player performs an action (e.g., “move character,” “shoot weapon”), call the corresponding server-side reducer using
SpacetimeDBClient.CallReducerAsync("move_player", x, y). The game logic within the reducer will validate the action and update the database, which then propagates back to all clients.
The key is mapping the real-time data from SpaceTimeDB to your game’s visual and logical components, and mapping player input to reducer calls.
Mini-Challenge: Enhance the Chat with User Colors
Let’s make our chat a bit more colorful!
Challenge: Modify the chat application so that each user (identified by their sender name) has a consistent, randomly assigned color for their messages. This color should be determined when they first send a message and remain the same for that user across all their messages and for all clients.
Hint:
- You’ll need a new table in your SpaceTimeDB schema (e.g.,
UserColor) to store whichsenderhas whichcolor. - Your
create_messagereducer will need to check if a color already exists for thesender. If not, it should generate a random color (e.g., a hex string like “#RRGGBB”) and store it in theUserColortable before inserting the message. - Your client-side code will need to subscribe to this new
UserColortable and use that information when rendering messages. You’ll need to fetch the color for each message’s sender.
What to observe/learn:
- How to extend your SpaceTimeDB schema and reducer logic to manage additional shared state.
- How to subscribe to multiple tables from the client.
- How to combine data from different subscribed tables on the client to enrich the UI.
- The deterministic nature of reducers: a random color generated within a reducer will be the same for everyone.
Common Pitfalls & Troubleshooting
Connection Issues (
Failed to connect to SpaceTimeDB):- Check SpaceTimeDB Server: Is your
spacetime db devinstance actually running? - Host/Port Mismatch: Ensure
SPACETIMEDB_HOSTandSPACETIMEDB_PORTin yourscript.jsexactly match where your SpaceTimeDB server is listening. - Firewall: Local firewalls can sometimes block WebSocket connections.
- Module Name: Double-check
SPACETIMEDB_DB_NAMEmatches the name of your SpaceTimeDB module.
- Check SpaceTimeDB Server: Is your
Reducer Call Errors (
Error calling create_message reducer):- Reducer Name Mismatch: Is the string passed to
SpacetimeDBClient.call()an exact match for your reducer function’s name inreducers.stdb? (e.g.,create_messagevsCreateMessage). Reducer names are case-sensitive. - Argument Mismatch: Do the number and types of arguments passed from the client match the reducer’s signature in Rust? (e.g.,
create_message(sender: String, text: String)expects two strings). - Reducer Logic Errors: Check your SpaceTimeDB server logs. If the reducer panics or returns an error, it will be reported there, and the client will receive an error.
- Reducer Name Mismatch: Is the string passed to
No Real-time Updates:
- Subscription Missing/Incorrect: Did you call
SpacetimeDBClient.subscribe()for the correct table names? - Event Listener Order: Ensure your
SpacetimeDBClient.on("initial_sync", ...)andSpacetimeDBClient.on("update", ...)listeners are registered before theinitial_syncorupdateevents might occur (i.e., immediately afterconnect). - Table Changes Not Occurring: Are your reducers actually modifying the tables you expect them to? Check the SpaceTimeDB CLI
db dumpordb watchcommands.
- Subscription Missing/Incorrect: Did you call
Identity Persistence Issues:
- If
localStorageisn’t working or is cleared, the client will get a newIdentityon each connection. This might be desired, but if you expect persistent recognition without full authentication, ensurelocalStorage.setItemandlocalStorage.getItemare working correctly.
- If
Remember, the SpaceTimeDB server logs are your best friend for debugging server-side reducer issues, and your browser’s developer console is essential for client-side debugging.
Summary
Phew! You’ve come a long way. In this chapter, we explored the crucial topic of integrating your SpaceTimeDB backend with frontend applications. Here’s a quick recap of what we covered:
- Client SDKs: These libraries (like
@clockworklabs/spacetimedb-sdkfor JavaScript/TypeScript) simplify connecting, subscribing, and calling reducers. - Real-time Synchronization: We learned how SpaceTimeDB pushes initial data and subsequent updates over WebSockets, enabling instant UI reactions.
- Reducer Invocation: Clients interact with the database by calling server-side reducers, ensuring centralized logic, determinism, and security.
- Practical Web Integration: We built a hands-on real-time chat application, demonstrating how to connect, subscribe to table changes, process updates, and invoke reducers from a vanilla JavaScript frontend.
- Game Engine Concepts: We conceptually discussed how similar principles apply to integrating with game engines like Unity using their respective SDKs.
- Troubleshooting: We looked at common issues like connection failures, reducer call errors, and missing real-time updates.
You now have the foundational knowledge to build truly interactive, real-time user experiences powered by SpaceTimeDB. The ability to seamlessly connect your frontend to a globally consistent, event-driven backend opens up a world of possibilities for multiplayer games, collaborative tools, and dynamic dashboards.
What’s Next?
In the next chapter, we’ll delve into more advanced aspects of SpaceTimeDB, focusing on topics like authentication, security models, and how to manage user access in your real-time applications.
References
- SpaceTimeDB Official Website
- SpaceTimeDB Documentation
- SpaceTimeDB GitHub Repository
- SpaceTimeDB JavaScript/TypeScript SDK (Refer to the
sdkcrate within the main repo for the JS/TS SDK’s source and usage)
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.