This guide covers how to connect to and interact with actors from client applications using RivetKit’s JavaScript/TypeScript client library.
Client Setup
Creating a Client
There are several ways to create a client for communicating with actors:
External Client Server-side Client Actor-to-Actor For frontend applications or external services connecting to your RivetKit backend:
import { createClient } from "@rivetkit/actor/client" ;
import type { registry } from "./registry" ;
const client = createClient < typeof registry >( "http://localhost:8080" );
This client communicates over HTTP/WebSocket and requires authentication.
For frontend applications or external services connecting to your RivetKit backend:
import { createClient } from "@rivetkit/actor/client" ;
import type { registry } from "./registry" ;
const client = createClient < typeof registry >( "http://localhost:8080" );
This client communicates over HTTP/WebSocket and requires authentication.
From your backend server that hosts the registry:
import { registry } from "./registry" ;
const { client } = registry . createServer ();
This client bypasses network calls and doesn’t require authentication.
From within an actor to communicate with other actors:
const myActor = actor ({
actions: {
callOtherActor : ( c ) => {
const client = c . client < typeof registry >();
return client . otherActor . getOrCreate ([ "id" ]);
}
}
});
Read more about communicating between actors .
Client Configuration
Configure the client with additional options:
const client = createClient < typeof registry >( "http://localhost:8080" , {
// Data serialization format
encoding: "cbor" , // or "json"
// Network transports in order of preference
supportedTransports: [ "websocket" , "sse" ]
});
Actor Handles
Returns a handle to an existing actor or null
if it doesn’t exist:
// Get existing actor by tags
const handle = client . myActor . get ([ "actor-id" ]);
if ( handle ) {
const result = await handle . someAction ();
} else {
console . log ( "Actor doesn't exist" );
}
Returns a handle to an existing actor or creates a new one if it doesn’t exist:
// Get or create actor (synchronous)
const counter = client . counter . getOrCreate ([ "my-counter" ]);
// With initialization input
const game = client . game . getOrCreate ([ "game-123" ], {
gameMode: "tournament" ,
maxPlayers: 8 ,
});
// Call actions immediately
const count = await counter . increment ( 5 );
get()
and getOrCreate()
are synchronous and return immediately. The actor is created lazily when you first call an action.
Explicitly creates a new actor instance, failing if one already exists:
// Create new actor (async)
const newGame = await client . game . create ([ "game-456" ], {
gameMode: "classic" ,
maxPlayers: 4 ,
});
// Actor is guaranteed to be newly created
await newGame . initialize ();
getWithId(id, opts?)
- Find by Internal ID
Connect to an actor using its internal system ID:
// Connect by internal ID
const actorId = "55425f42-82f8-451f-82c1-6227c83c9372" ;
const actor = client . myActor . getWithId ( actorId );
await actor . performAction ();
Prefer using tags over internal IDs for actor discovery. IDs are primarily for debugging and advanced use cases.
Actions
Calling Actions
Once you have an actor handle, call actions directly. All action calls are async:
const counter = client . counter . getOrCreate ([ "my-counter" ]);
// Call action with no arguments
const currentCount = await counter . getCount ();
// Call action with arguments
const newCount = await counter . increment ( 5 );
// Call action with object parameter
await counter . updateSettings ({
step: 2 ,
maximum: 100 ,
});
Action Parameters
Actions receive parameters exactly as defined in the actor:
// Actor definition
const chatRoom = actor ({
actions: {
sendMessage : ( c , userId : string , message : string , metadata ?: object ) => {
// Action implementation
}
}
});
// Client usage - parameters match exactly
await chatRoom . sendMessage ( "user-123" , "Hello!" , { priority: "high" });
Error Handling
Handle action errors appropriately:
try {
const result = await counter . increment ( 5 );
} catch ( error ) {
if ( error . code === "RATE_LIMITED" ) {
console . log ( "Too many requests, try again later" );
} else {
console . error ( "Action failed:" , error . message );
}
}
Real-time Connections
Real-time connections enable bidirectional communication between clients and actors through persistent connections. RivetKit automatically negotiates between WebSocket (preferred for full duplex) and Server-Sent Events (SSE) as a fallback for restrictive environments.
connect(params?)
- Establish Stateful Connection
For real-time communication with events, use .connect()
:
const counter = client . counter . getOrCreate ([ "live-counter" ]);
const connection = counter . connect ();
// Listen for events
connection . on ( "countChanged" , ( newCount : number ) => {
console . log ( "Count updated:" , newCount );
});
// Call actions through the connection
const result = await connection . increment ( 1 );
// Clean up when done
await connection . dispose ();
Events
on(eventName, callback)
- Listen for Events
Listen for events from the actor:
// Listen for chat messages
connection . on ( "messageReceived" , ( message ) => {
console . log ( ` ${ message . from } : ${ message . text } ` );
});
// Listen for game state updates
connection . on ( "gameStateChanged" , ( gameState ) => {
updateUI ( gameState );
});
// Listen for player events
connection . on ( "playerJoined" , ( player ) => {
console . log ( ` ${ player . name } joined the game` );
});
once(eventName, callback)
- Listen Once
Listen for an event only once:
// Wait for game to start
connection . once ( "gameStarted" , () => {
console . log ( "Game has started!" );
});
off(eventName, callback?)
- Stop Listening
Remove event listeners:
const messageHandler = ( message ) => console . log ( message );
// Add listener
connection . on ( "messageReceived" , messageHandler );
// Remove specific listener
connection . off ( "messageReceived" , messageHandler );
// Remove all listeners for event
connection . off ( "messageReceived" );
dispose()
- Clean Up Connection
Always dispose of connections when finished to free up resources:
const connection = actor . connect ();
try {
// Use the connection
connection . on ( "event" , handler );
await connection . someAction ();
} finally {
// Clean up the connection
await connection . dispose ();
}
// Or with automatic cleanup in React/frameworks
useEffect (() => {
const connection = actor . connect ();
return () => {
connection . dispose ();
};
}, []);
Important: Disposing a connection:
Closes the underlying WebSocket or SSE connection
Removes all event listeners
Cancels any pending reconnection attempts
Prevents memory leaks in long-running applications
Transports
Connections automatically negotiate the best available transport:
WebSocket Transport
Full duplex : Client can send and receive
Low latency : Immediate bidirectional communication
Preferred : Used when available
Server-Sent Events (SSE)
Server-to-client : Events only, actions via HTTP
Fallback : Used when WebSocket unavailable
Compatibility : Works in restrictive environments
Reconnections
Connections automatically handle network failures with robust reconnection logic:
Automatic Behavior:
Exponential backoff : Retry delays increase progressively to avoid overwhelming the server
Action queuing : Actions called while disconnected are queued and sent once reconnected
Event resubscription : Event listeners are automatically restored on reconnection
State synchronization : Connection state is preserved and synchronized after reconnection
Authentication
Connection Parameters
Pass authentication data when connecting to actors:
// With connection parameters
const chat = client . chatRoom . getOrCreate ([ "general" ]);
const connection = chat . connect ({
authToken: "jwt-token-here" ,
userId: "user-123" ,
displayName: "Alice"
});
// Parameters available in actor via onAuth hook
// Or for action calls
const result = await chat . sendMessage ( "Hello world!" , {
authToken: "jwt-token-here"
});
onAuth Hook Validation
Actors can validate authentication using the onAuth
hook:
import { actor , Forbidden } from "@rivetkit/actor" ;
const protectedActor = actor ({
onAuth : async ( opts ) => {
const { req , params } = opts ;
// Extract token from params or headers
const token = params . authToken || req . headers . get ( "Authorization" );
if (! token ) {
throw new Forbidden ( "Authentication required" );
}
// Validate and return user data
const user = await validateJWT ( token );
return { userId: user . id , role: user . role };
},
actions: {
protectedAction : ( c , data : string ) => {
// Access auth data via c.conn.auth
const { userId , role } = c . conn . auth ;
if ( role !== "admin" ) {
throw new Forbidden ( "Admin access required" );
}
return `Hello admin ${ userId } ` ;
}
}
});
Learn more about authentication patterns .
Type Safety
RivetKit provides end-to-end type safety between clients and actors:
Action Type Safety
TypeScript validates action signatures and return types:
// TypeScript knows the action signatures
const counter = client . counter . getOrCreate ([ "my-counter" ]);
const count : number = await counter . increment ( 5 ); // ✓ Correct
const invalid = await counter . increment ( "5" ); // ✗ Type error
// IDE autocomplete shows available actions
counter . /* <IDE shows: increment, decrement, getCount, reset> */
Client Type Safety
Import types from your registry for full type safety:
import type { registry } from "./registry" ;
// Client is fully typed
const client = createClient < typeof registry >( "http://localhost:8080" );
// IDE provides autocomplete for all actors
client . /* <shows: counter, chatRoom, gameRoom, etc.> */
Best Practices
Actions vs Connections
Use Stateless Actions For:
Simple request-response operations
One-off operations
Server-side integration
Minimal overhead required
// Good for simple operations
const result = await counter . increment ( 1 );
const status = await server . getStatus ();
Use Stateful Connections For:
Real-time updates needed
Multiple related operations
Event-driven interactions
Long-lived client sessions
// Good for real-time features
const connection = chatRoom . connect ();
connection . on ( "messageReceived" , updateUI );
await connection . sendMessage ( "Hello!" );
Resource Management
Always clean up connections when finished:
// Manual cleanup
const connection = actor . connect ();
try {
// Use connection
connection . on ( "event" , handler );
await connection . action ();
} finally {
await connection . dispose ();
}
// Automatic cleanup with lifecycle
connection . on ( "disconnected" , () => {
console . log ( "Connection cleaned up" );
});
Error Handling
Implement proper error handling for both actions and connections:
// Action error handling
try {
const result = await counter . increment ( 5 );
} catch ( error ) {
if ( error . code === "RATE_LIMITED" ) {
console . log ( "Rate limited, try again later" );
} else if ( error . code === "UNAUTHORIZED" ) {
redirectToLogin ();
} else {
console . error ( "Action failed:" , error . message );
}
}
// Connection error handling
connection . on ( "error" , ( error ) => {
console . error ( "Connection error:" , error );
// Implement reconnection logic if needed
});
Use appropriate patterns for optimal performance:
// Batch multiple operations through a connection
const connection = actor . connect ();
await Promise . all ([
connection . operation1 (),
connection . operation2 (),
connection . operation3 (),
]);
// Use getOrCreate for actors you expect to exist
const existing = client . counter . getOrCreate ([ "known-counter" ]);
// Use create only when you need a fresh instance
const fresh = await client . counter . create ([ "new-counter" ]);