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:
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:
Takes a username and message as parameters
Adds the message to the actor's state for persistence
Broadcasts the message to all connected clients
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.
Complete Chat Room Actor Code
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:
Sign in to your Rivet account
Create or select a project
Choose an environment
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 : 800 px ; margin : 0 auto ; padding : 20 px ; }
#message-list { height : 400 px ; overflow-y : auto ; list-style : none ; padding : 10 px ; border : 1 px solid #ccc ; margin-bottom : 10 px ; }
#message-form { display : flex ; }
#message-input { flex : 1 ; padding : 8 px ; }
button { padding : 8 px 16 px ; 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 : 800 px ; margin : 0 auto ; padding : 20 px ; }
#message-list { height : 400 px ; overflow-y : auto ; list-style : none ; padding : 10 px ; border : 1 px solid #ccc ; margin-bottom : 10 px ; }
#message-form { display : flex ; }
#message-input { flex : 1 ; padding : 8 px ; }
button { padding : 8 px 16 px ; 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 >