Sign In
Guides

Building a Realtime Chat App with Actors

In this guide, we're building a realtime chat application using the ActorCore framework. The app consists of:

  • ChatRoom Actor: A server-side component that:

    • Uses tags to create separate chat channels
    • Stores the message history in persistent state
    • Provides methods for sending messages and retrieving history
    • Broadcasts new messages to all connected clients
  • Web Client: A browser-based UI that:

    • Prompts users for username and channel name
    • Connects to the appropriate channel via tags
    • Displays the chat interface
    • Loads message history on connection
    • Shows new messages in realtime
    • Allows users to send messages

Set up your project

Create a new actor project:

Command Line
npx create-actor@latest chat-room -p rivet -t counter

This command creates a new Rivet actor project with the necessary configuration files and dependencies. We're using the counter template as a starting point and will modify it for our chat application.


Define the Chat Room actor

Create a file called src/chat-room.ts and add the base class structure:

TypeScript
import { Actor, type Rpc } from "actor-core";

// State managed by the actor
export interface State {
  messages: { username: string; message: string }[];
}

export default class ChatRoom extends Actor<State> {
  // Methods will be added in the following steps
}

Step 1: Initialize the actor state

First, add the _onInitialize method to set up the initial state:

TypeScript
export default class ChatRoom extends Actor<State> {
  _onInitialize() {
    return { messages: [] };
  }
}

This method runs when the actor is first created, initializing an empty messages array.

Step 2: Add message sending functionality

Next, add the method to send messages:

TypeScript
export default class ChatRoom extends Actor<State> {
  // ...previous code...
  
  sendMessage(
    _rpc: Rpc<ChatRoom>,
    username: string, 
    message: string
  ): void {
    // Save message to persistent storage
    this._state.messages.push({ username, message });

    // Broadcast message to all connected clients
    this._broadcast("newMessage", username, message);
  }
}

This method:

  • Takes a username and message as parameters
  • Adds the message to the actor's state for persistence
  • Broadcasts the message to all connected clients

Step 3: Add history retrieval

Finally, add a method to retrieve chat history:

TypeScript
export default class ChatRoom extends Actor<State> {
  // ...previous code...
  
  getHistory(_rpc: Rpc<ChatRoom>): { username: string; message: string }[] {
    return this._state.messages;
  }
}

This method allows clients to fetch all previous messages when they connect.

Step 4: Deploy to Rivet

Deploy your actor with:

Command Line
cd chat-room
npm run deploy

Follow the prompts to:

  1. Sign in to your Rivet account
  2. Create or select a project
  3. Choose an environment

After deployment, you'll receive your Actor Manager URL, which clients will use to connect to your chat room.


Build a web client

Create a simple web client to interact with your chat room:

Step 1: Create the HTML structure

<!DOCTYPE html>
<html>
<head>
  <title>Rivet Chat Room</title>
  <style>
    body { font-family: sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
    #message-list { height: 400px; overflow-y: auto; list-style: none; padding: 10px; border: 1px solid #ccc; margin-bottom: 10px; }
    #message-form { display: flex; }
    #message-input { flex: 1; padding: 8px; }
    button { padding: 8px 16px; background: #0070f3; color: white; border: none; }
  </style>
</head>
<body>
  <h1>Rivet Chat Room</h1>
  <ul id="message-list"></ul>
  <form id="message-form">
    <input id="message-input" placeholder="Type a message..." autocomplete="off">
    <button type="submit">Send</button>
  </form>
</body>
</html>

Step 2: Add the client script

Add this script tag just before the closing </head> tag:

<script type="module">
  import { Client } from 'https://unpkg.com/actor-core/dist/browser/index.js';

  // Replace with your Actor Manager URL from deployment
  const client = new Client('https://your-actor-manager-url.rivet.run');

  let username = prompt('Enter your username:');
  if (!username) username = 'Anonymous';
  
  let channel = prompt('Enter channel name:', 'general');
  if (!channel) channel = 'general';

  async function init() {
    // Connect to chat room with channel tag
    const chatRoom = await client.get({ 
      name: 'chat-room',
      channel,  // Use channel as a tag to separate different chat rooms
    });
    
    // Store reference for use in event handlers
    // In a production app, you'd use a more encapsulated approach
    window.chatRoom = chatRoom;
  }

  init().catch(console.error);
</script>

Step 3: Load messages and listen for updates

Update your init function and add the addMessage helper function:

<script type="module">
  // ...previous code...

  async function init() {
    // ...previous code...

    try {
      // Load chat history
      const messages = await chatRoom.getHistory();
      messages.forEach(msg => {
        addMessage(msg.username, msg.message);
      });

      // Listen for new messages
      chatRoom.on('newMessage', (username, message) => {
        addMessage(username, message);
      });
    } catch (error) {
      console.error("Failed to load chat history:", error);
      alert("Error loading chat history. Please try refreshing the page.");
    }
  }

  function addMessage(username, message) {
    const messageList = document.getElementById('message-list');
    const item = document.createElement('li');
    
    // Create elements instead of using innerHTML to prevent XSS
    const usernameSpan = document.createElement('strong');
    usernameSpan.textContent = username;
    
    item.appendChild(usernameSpan);
    item.appendChild(document.createTextNode(': ' + message));
    
    messageList.appendChild(item);
    messageList.scrollTop = messageList.scrollHeight;
  }
</script>

Step 4: Handle sending messages

Add the form submit handler to your init function:

<script type="module">
  // ...previous code...

  async function init() {
    // ...previous code...

    // Update page title with channel name
    document.title = `Chat: ${channel}`;
    
    // Add channel name to the UI
    const heading = document.querySelector('h1');
    heading.textContent = `Rivet Chat Room - ${channel}`;

    // Send message on form submit
    document.getElementById('message-form').addEventListener('submit', async (e) => {
      e.preventDefault();
      const input = document.getElementById('message-input');
      const message = input.value.trim();
      
      if (message) {
        try {
          await chatRoom.sendMessage(username, message);
          input.value = '';
        } catch (error) {
          console.error("Failed to send message:", error);
          alert("Error sending message. Please try again.");
        }
      }
    });
  }
</script>
Suggest changes to this page