website/public/guides/chat.md
In this guide, we're building a realtime chat application using the Rivet library. The app consists of:
ChatRoom Actor: A server-side component that:
Web Client: A browser-based UI that:
Create a new actor project:
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.
Create a file called src/chat-room.ts and add the base class structure:
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
}
First, add the _onInitialize method to set up the initial state:
export default class ChatRoom extends Actor<State> {
_onInitialize() {
return { messages: [] };
}
}
This method runs when the actor is first created, initializing an empty messages array.
Next, add the method to send messages:
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:
Finally, add a method to retrieve chat history:
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.
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> {
_onInitialize(): State {
return { messages: [] };
}
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
// Event name is 'newMessage', clients can listen for this event
this._broadcast("newMessage", username, message);
}
getHistory(_rpc: Rpc<ChatRoom>): { username: string; message: string }[] {
return this._state.messages;
}
}
Deploy your actor with:
cd chat-room
npm run deploy
Follow the prompts to:
After deployment, you'll receive your Actor Manager URL, which clients will use to connect to your chat room.
Create a simple web client to interact with your chat room:
<!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>
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>
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>
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>
<!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>
<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;
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.");
}
// 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.");
}
}
});
}
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;
}
init().catch(console.error);
</script>
</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>