Sign In
More

Actor Clients

Learn how to call actions and connect to actors from client applications using Rivet's TypeScript client library.

Rivet also supports React and Rust clients.

Using the RivetKit client is completely optional. If you prefer to write your own networking logic, you can either:

  • Make HTTP requests directly to the registry (see OpenAPI spec)
  • Write your own HTTP endpoints and use the client returned from registry.runServer (see below)

Client Setup

There are several ways to create a client for communicating with actors:

Server-side client diagram

From your backend server that hosts the registry:

server.ts
import { registry } from "./registry";

const { client, serve } = registry.createServer();

const app = new Hono();

app.post("/foo", () => {
	const response = await client.myActor.getOrCreate("my-id").bar();
	return c.json(response);
});

serve(app);

This client doesn't require authentication.

ActorClient

The ActorClient provides methods for finding and creating actors. All methods return an ActorHandle that you can use to call actions or establish connections.

get(key?, opts?) - Find Existing Actor

Returns a handle to an existing actor or null if it doesn't exist:

TypeScript
// Get existing actor by key
const handle = client.myActor.get(["actor-id"]);

if (handle) {
  const result = await handle.someAction();
} else {
  console.log("Actor doesn't exist");
}

getOrCreate(key?, opts?) - Find or Create Actor

Returns a handle to an existing actor or creates a new one if it doesn't exist:

TypeScript
// Get or create actor (synchronous)
const counter = client.counter.getOrCreate(["my-counter"]);

// With initialization input
const game = client.game.getOrCreate(["game-123"], {
  createWithInput: {
    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.

create(key?, opts?) - Create New Actor

Explicitly creates a new actor instance, failing if one already exists:

TypeScript
// Create new actor (async)
const newGame = await client.game.create(["game-456"], {
  input: {
    gameMode: "classic",
    maxPlayers: 4,
  }
});

// Actor is guaranteed to be newly created
await newGame.initialize();

getForId(id, opts?) - Find by Internal ID

Connect to an actor using its internal system ID:

TypeScript
// Connect by internal ID
const actorId = "55425f42-82f8-451f-82c1-6227c83c9372";
const actor = client.myActor.getForId(actorId);

await actor.performAction();

Prefer using keys over internal IDs for actor discovery. IDs are primarily for debugging and advanced use cases.

ActorHandle

An ActorHandle represents a reference to an actor instance and provides methods for calling actions and establishing connections. You get an ActorHandle from the ActorClient methods like get(), getOrCreate(), and create().

Calling Actions

You can call actions directly on an ActorHandle:

TypeScript
const counter = client.counter.getOrCreate(["my-counter"]);

// Call actions directly
const count = await counter.increment(5);
const currentValue = await counter.getCount();
await counter.reset();

Actions called on an ActorHandle are stateless - each call is independent and doesn't maintain a persistent connection to the actor.

fetch(input, init?) - Raw HTTP Requests

Make direct HTTP requests to the actor's onFetch handler:

TypeScript
const actor = client.myActor.getOrCreate(["key"]);

// GET request
const response = await actor.fetch("/api/hello", {
  method: "GET"
});
const data = await response.json();

// POST request with body
const postResponse = await actor.fetch("/api/echo", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ message: "Hello world" })
});

// Can also pass a Request object
const request = new Request("/api/data", { method: "GET" });
const requestResponse = await actor.fetch(request);

See Fetch & WebSocket Handler documentation for more information.

For most use cases, actions provide a higher-level API that's easier to work with than raw HTTP handlers.

websocket(path?, protocols?) - Raw WebSocket Connections

Create direct WebSocket connections to the actor's onWebSocket handler:

TypeScript
const actor = client.myActor.getOrCreate(["key"]);

// Basic WebSocket connection
const ws = await actor.websocket();
ws.addEventListener("message", (event) => {
  console.log("Message:", event.data);
});
ws.send("Hello WebSocket!");

// WebSocket with custom path
const streamWs = await actor.websocket("/stream");

// WebSocket with protocols
const protocolWs = await actor.websocket("/", ["chat", "v1"]);

See Fetch & WebSocket Handler documentation for more information.

For most use cases, actions & events provide a higher-level API that's easier to work with than raw HTTP handlers.

connect(params?) - Establish Stateful Connection

To open a stateful connection using ActorConn, call .connect():

TypeScript
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();

ActorConn

Real-time connections enable bidirectional communication between clients and actors through persistent connections. Rivet automatically negotiates between WebSocket (preferred for full duplex) and Server-Sent Events (SSE) as a fallback for restrictive environments.

For more information on connections, see the connections documentation. For more information on handling events, see the events documentation.

Calling Actions

You can also call actions through an ActorConn, just like with an ActorHandle:

TypeScript
const connection = counter.connect();

// Call actions through the connection
const count = await connection.increment(5);
const currentValue = await connection.getCount();

Reconnections

Connections automatically handle network failures with built-in reconnection logic:

  • 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

on(eventName, callback) - Listen for Events

Listen for events from the actor:

TypeScript
// 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:

TypeScript
// Wait for game to start
connection.once("gameStarted", () => {
  console.log("Game has started!");
});

dispose() - Clean Up Connection

Always dispose of connections when finished to free up resources:

TypeScript
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

Authentication

Connection Parameters

Pass authentication data when connecting to actors:

TypeScript
// With connection parameters
const chat = client.chatRoom.getOrCreate(["general"], {
  params: {
    authToken: "jwt-token-here",
    userId: "user-123",
    displayName: "Alice"
  }
});

const connection = chat.connect();

// Or for action calls
const result = await chat.sendMessage("Hello world!");

onAuth Hook Validation

Actors can validate authentication using the onAuth hook:

TypeScript
import { actor, UserError } from "@rivetkit/actor";

const protectedActor = actor({
  onAuth: async (opts) => {
    const { req, params, intents } = opts;
    
    // Extract token from params or headers
    const token = params.authToken || req.headers.get("Authorization");
    
    if (!token) {
      throw new UserError("Authentication required", { code: "missing_auth" });
    }
    
    // Validate and return user data
    const user = await validateJWT(token);
    
    // Check permissions based on what the client is trying to do
    if (intents.has("create") && user.role !== "admin") {
      throw new UserError("Admin access required", { code: "insufficient_permissions" });
    }
    
    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 UserError("Admin access required", { code: "insufficient_permissions" });
      }
      
      return `Hello admin ${userId}`;
    }
  }
});

Learn more about authentication patterns.

Type Safety

Rivet provides end-to-end type safety between clients and actors:

Action Type Safety

TypeScript validates action signatures and return types:

TypeScript
// 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:

TypeScript
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

ActorHandle (Stateless) vs ActorConn (Stateful) Clients

Use ActorHandle (Stateless) For:

  • Simple request-response operations
  • One-off operations
  • Server-side integration
TypeScript
// Good for simple operations
const result = await counter.increment(1);
const status = await server.getStatus();

Use ActorConn (Stateful) Connections For:

  • Real-time updates needed
  • Event-driven interactions
  • Long-lived client sessions
TypeScript
// 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:

TypeScript
// Manual cleanup
const connection = actor.connect();
try {
  // Use connection
  connection.on("event", handler);
  await connection.action();
} finally {
  await connection.dispose();
}

Error Handling

Handle connection errors using the .onError() method:

const connection = actor.connect();

connection.onError((error) => {
  console.error('Connection error:', error);
  showErrorMessage(error.message);
  
  // Handle specific error types
  if (error.code === 'UNAUTHORIZED') {
    redirectToLogin();
  } else if (error.code === 'ACTOR_NOT_FOUND') {
    showActorNotFoundError();
  }
});

Execute Actions in Parallel

You can execute batch requests in parallel:

TypeScript
// 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"]);

However, it's recommended to move this logic to run within the actor instead of the client if executing multiple actions is a common pattern.

Suggest changes to this page