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:

  1. 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.”
  2. 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.
  3. 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:

flowchart LR subgraph Client_App["Client Application"] UI[User Interface] --> Reducer_Call[Call Reducer] Data_Display[Display Real time Data] <-- Data_Update[Real time Data Updates] Reducer_Call --> SDK[SpaceTimeDB SDK] Data_Update --> SDK end subgraph SpaceTimeDB_Server["SpaceTimeDB Server"] DB[Database State] Reducers[Reducer Functions] WS_Server[WebSocket Server] end SDK <--->|WebSocket Connection| WS_Server WS_Server --->|Reducer Invocation| Reducers Reducers --->|Mutate State| DB DB --->|Propagate Changes| WS_Server WS_Server --->|Push Updates| SDK
  • 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:

  1. A running SpaceTimeDB instance (e.g., on localhost:3000).
  2. A SpaceTimeDB module with a Message table and a create_message reducer.
    • 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.

  1. Initialize Node.js project:

    npm init -y
    

    This creates a package.json file.

  2. Install SpaceTimeDB SDK:

    npm install @clockworklabs/[email protected] --save-exact
    

    We’re explicitly installing v2.0.1 as of 2026-03-14, which is a recent stable release for the SDK. The --save-exact flag ensures this specific version is recorded.

    Why this package? The @clockworklabs/spacetimedb-sdk is 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:

  1. import { SpacetimeDBClient, Identity } from '@clockworklabs/spacetimedb-sdk';: We import the necessary classes from the SDK. SpacetimeDBClient is our main entry point, and Identity is used for client identification.
  2. Configuration: SPACETIMEDB_HOST, SPACETIMEDB_PORT, and SPACETIMEDB_DB_NAME must match your running SpaceTimeDB instance.
  3. 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 from localStorage. 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!).
  4. SpacetimeDBClient.subscribe([{ tableName: 'Message' }]): Once connected, we tell SpaceTimeDB which tables we are interested in. Here, we subscribe to the Message table. This means the client will receive the initial data for this table and all subsequent changes.
  5. SpacetimeDBClient.on("initial_sync", ...): This event listener fires once when the client first receives the complete initial state of all subscribed tables. We use SpacetimeDBClient.getEntities('Message') to fetch all current messages and render them.
  6. SpacetimeDBClient.on("update", ...): This is where the real-time magic happens! This event fires every time there’s a change in any subscribed table. The events array contains detailed information about what changed (e.g., event_type: 'insert', table_name: 'Message', row_new with the inserted data). We filter for new Message inserts and append them to our chat display.
  7. SpacetimeDBClient.on("disconnect", ...): A simple handler for when the connection drops, attempting to reconnect after a delay.
  8. 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 in reducers.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:

  1. Install http-server (if you don’t have it):
    npm install -g http-server
    
  2. Run the server from your chat-frontend directory:
    http-server .
    
    This will serve your index.html on http://localhost:8080 (or another port).

Step 5: Test Your Real-time Chat

  1. Ensure your SpaceTimeDB instance is running (e.g., spacetime db dev).
  2. Open your browser to http://localhost:8080.
  3. Open a second browser tab or window to the same address.
  4. 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.

  1. Install C# SDK: Add the SpaceTimeDB C# SDK package to your Unity project. This is typically done via a .unitypackage or NuGet.
  2. Connect: In a MonoBehaviour script (e.g., GameManager.cs), use SpacetimeDBClient.ConnectAsync() to establish the connection to your SpaceTimeDB server.
  3. Subscribe: Call SpacetimeDBClient.Subscribe() to specify which tables your game needs to observe (e.g., Player, Enemy, Projectile).
  4. Handle Updates:
    • Register callbacks for SpacetimeDBClient.onUpdate or 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.
  5. 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 which sender has which color.
  • Your create_message reducer will need to check if a color already exists for the sender. If not, it should generate a random color (e.g., a hex string like “#RRGGBB”) and store it in the UserColor table before inserting the message.
  • Your client-side code will need to subscribe to this new UserColor table 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

  1. Connection Issues (Failed to connect to SpaceTimeDB):

    • Check SpaceTimeDB Server: Is your spacetime db dev instance actually running?
    • Host/Port Mismatch: Ensure SPACETIMEDB_HOST and SPACETIMEDB_PORT in your script.js exactly match where your SpaceTimeDB server is listening.
    • Firewall: Local firewalls can sometimes block WebSocket connections.
    • Module Name: Double-check SPACETIMEDB_DB_NAME matches the name of your SpaceTimeDB module.
  2. 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 in reducers.stdb? (e.g., create_message vs CreateMessage). 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.
  3. 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", ...) and SpacetimeDBClient.on("update", ...) listeners are registered before the initial_sync or update events might occur (i.e., immediately after connect).
    • Table Changes Not Occurring: Are your reducers actually modifying the tables you expect them to? Check the SpaceTimeDB CLI db dump or db watch commands.
  4. Identity Persistence Issues:

    • If localStorage isn’t working or is cleared, the client will get a new Identity on each connection. This might be desired, but if you expect persistent recognition without full authentication, ensure localStorage.setItem and localStorage.getItem are working correctly.

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-sdk for 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

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