Concepts

Crash Course

Actors for long-lived processes with durable state, realtime, and hibernate when not in use.

Features

  • Long-Lived, Stateful Compute: Each unit of compute is like a tiny server that remembers things between requests – no need to re-fetch data from a database or worry about timeouts. Like AWS Lambda, but with memory and no timeouts.
  • Blazing-Fast Reads & Writes: State is stored on the same machine as your compute, so reads and writes are ultra-fast. No database round trips, no latency spikes. State is persisted to Rivet for long term storage, so it survives server restarts.
  • Realtime: Update state and broadcast changes in realtime with WebSockets. No external pub/sub systems, no polling – just built-in low-latency events.
  • Infinitely Scalable: Automatically scale from zero to millions of concurrent actors. Pay only for what you use with instant scaling and no cold starts.
  • Fault Tolerant: Built-in error handling and recovery. Actors automatically restart on failure while preserving state integrity and continuing operations.

When to Use Rivet Actors

  • AI agents & sandboxes: multi-step toolchains, conversation memory, sandbox orchestration.
  • Multiplayer or collaborative apps: CRDT docs, shared cursors, realtime dashboards, chat.
  • Workflow automation: background jobs, cron, rate limiters, durable queues, backpressure control.
  • Data-intensive backends: geo-distributed or per-tenant databases, in-memory caches, sharded SQL.
  • Networking workloads: WebSocket servers, custom protocols, local-first sync, edge fanout.

Common Patterns

Actors scale naturally through isolated state and message-passing. Structure your applications with these patterns:

Design Patterns Documentation

Actor Per Entity

Create one actor per user, document, or room. Use compound keys to scope entities:

Coordinator & Data Actors

Data actors handle core logic (chat rooms, game sessions, user data). Coordinator actors track and manage collections of data actors—think of them as an index.

Sharding

Split high-load actors by time, user ID, or random key:

Fan-In & Fan-Out

Distribute work across workers (fan-out) and aggregate results (fan-in):

import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

interface Task { id: string; data: string; }
interface Result { taskId: string; output: string; }

const coordinator = actor({
  state: { results: [] as Result[] },
  actions: {
    // Fan-out: distribute work in parallel
    startJob: async (c, tasks: Task[]) => {
      const client = c.client<typeof registry>();
      await Promise.all(
        tasks.map(t => client.worker.getOrCreate([t.id]).process(t))
      );
    },
    // Fan-in: collect results
    reportResult: (c, result: Result) => { c.state.results.push(result); },
  },
});

const worker = actor({
  state: {},
  actions: {
    process: async (c, task: Task) => {
      const result = { taskId: task.id, output: `Processed ${task.data}` };
      const client = c.client<typeof registry>();
      await client.coordinator.getOrCreate(["org-acme"]).reportResult(result);
    },
  },
});

const registry = setup({ use: { coordinator, worker } });
TypeScript

Anti-Patterns

”God” actor

Avoid a single actor that handles everything. This creates a bottleneck and defeats the purpose of the actor model. Split into focused actors per entity instead.

Actor-per-request

Actors maintain state across requests. Creating one per request wastes resources and loses the benefits of persistent state. Use actors for persistent entities and regular functions for stateless work.

Minimal Project

Backend

actors.ts

import { actor, setup } from "rivetkit";

const counter = actor({
  state: { count: 0 },
  actions: {
    increment: (c, amount: number) => {
      c.state.count += amount;
      c.broadcast("count", c.state.count);
      return c.state.count;
    },
  },
});

export const registry = setup({
  use: { counter },
});
TypeScript

server.ts

Integrate with the user’s existing server if applicable. Otherwise, default to Hono.

Minimal Client

import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const counter = actor({
  state: { count: 0 },
  actions: { increment: (c, amount: number) => c.state.count += amount }
});

const registry = setup({ use: { counter } });
const client = createClient<typeof registry>();
const counterHandle = client.counter.getOrCreate(["my-counter"]);
await counterHandle.increment(1);
TypeScript

See the client quick reference for more details.

Actor Quick Reference

State

Persistent data that survives restarts, crashes, and deployments. State is persisted on Rivet Cloud or Rivet self-hosted, so it survives restarts if the current process crashes or exits.

Documentation

Keys

Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing:

import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const chatRoom = actor({
  state: { messages: [] as string[] },
  actions: {
    getRoomInfo: (c) => ({ org: c.key[0], room: c.key[1] }),
  },
});

const registry = setup({ use: { chatRoom } });
const client = createClient<typeof registry>();

// Compound key: [org, room]
client.chatRoom.getOrCreate(["org-acme", "general"]);

// Access key inside actor via c.key
TypeScript

Don’t build keys with string interpolation like "org:${userId}" when userId contains user data. Use arrays instead to prevent key injection attacks.

Documentation

Input

Pass initialization data when creating actors.

import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const game = actor({
  createState: (c, input: { mode: string }) => ({ mode: input.mode }),
  actions: {},
});

const registry = setup({ use: { game } });
const client = createClient<typeof registry>();

// Client usage
const gameHandle = client.game.getOrCreate(["game-1"], {
  createWithInput: { mode: "ranked" }
});
TypeScript

Documentation

Temporary Variables

Temporary data that doesn’t survive restarts. Use for non-serializable objects (event emitters, connections, etc).

Documentation

Actions

Actions are the primary way clients and other actors communicate with an actor.

import { actor } from "rivetkit";

const counter = actor({
  state: { count: 0 },
  actions: {
    increment: (c, amount: number) => (c.state.count += amount),
    getCount: (c) => c.state.count,
  },
});
TypeScript

Documentation

Events & Broadcasts

Events enable real-time communication from actors to connected clients.

import { actor } from "rivetkit";

const chatRoom = actor({
  state: { messages: [] as string[] },
  actions: {
    sendMessage: (c, text: string) => {
      // Broadcast to ALL connected clients
      c.broadcast("newMessage", { text });
    },
  },
});
TypeScript

Documentation

Connections

Access the current connection via c.conn or all connected clients via c.conns. Use c.conn.id or c.conn.state to securely identify who is calling an action. Connection state is initialized via connState or createConnState, which receives parameters passed by the client on connect.

Documentation

Actor-to-Actor Communication

Actors can call other actors using c.client().

import { actor, setup } from "rivetkit";

const inventory = actor({
  state: { stock: 100 },
  actions: {
    reserve: (c, amount: number) => { c.state.stock -= amount; }
  }
});

const order = actor({
  state: {},
  actions: {
    process: async (c) => {
      const client = c.client<typeof registry>();
      await client.inventory.getOrCreate(["main"]).reserve(1);
    },
  },
});

const registry = setup({ use: { inventory, order } });
TypeScript

Documentation

Scheduling

Schedule actions to run after a delay or at a specific time. Schedules persist across restarts, upgrades, and crashes.

import { actor } from "rivetkit";

const reminder = actor({
  state: { message: "" },
  actions: {
    // Schedule action to run after delay (ms)
    setReminder: (c, message: string, delayMs: number) => {
      c.state.message = message;
      c.schedule.after(delayMs, "sendReminder");
    },
    // Schedule action to run at specific timestamp
    setReminderAt: (c, message: string, timestamp: number) => {
      c.state.message = message;
      c.schedule.at(timestamp, "sendReminder");
    },
    sendReminder: (c) => {
      c.broadcast("reminder", { message: c.state.message });
    },
  },
});
TypeScript

Documentation

Destroying Actors

Permanently delete an actor and its state using c.destroy().

import { actor } from "rivetkit";

const userAccount = actor({
  state: { email: "", name: "" },
  onDestroy: (c) => {
    console.log(`Account ${c.state.email} deleted`);
  },
  actions: {
    deleteAccount: (c) => {
      c.destroy();
    },
  },
});
TypeScript

Documentation

Lifecycle Hooks

Actors support hooks for initialization, connections, networking, and state changes.

import { actor } from "rivetkit";

interface RoomState {
  users: Record<string, boolean>;
  name?: string;
}

interface RoomInput {
  roomName: string;
}

interface ConnState {
  userId: string;
  joinedAt: number;
}

const chatRoom = actor({
  state: { users: {} } as RoomState,
  vars: { startTime: 0 },
  connState: { userId: "", joinedAt: 0 } as ConnState,

  // State & vars initialization
  createState: (c, input: RoomInput): RoomState => ({ users: {}, name: input.roomName }),
  createVars: () => ({ startTime: Date.now() }),

  // Actor lifecycle
  onCreate: (c) => console.log("created", c.key),
  onDestroy: (c) => console.log("destroyed"),
  onWake: (c) => console.log("actor started"),
  onSleep: (c) => console.log("actor sleeping"),
  onStateChange: (c, newState) => c.broadcast("stateChanged", newState),

  // Connection lifecycle
  createConnState: (c, params): ConnState => ({ userId: (params as { userId: string }).userId, joinedAt: Date.now() }),
  onBeforeConnect: (c, params) => { /* validate auth */ },
  onConnect: (c, conn) => console.log("connected:", conn.state.userId),
  onDisconnect: (c, conn) => console.log("disconnected:", conn.state.userId),

  // Networking
  onRequest: (c, req) => new Response(JSON.stringify(c.state)),
  onWebSocket: (c, socket) => socket.addEventListener("message", console.log),

  // Response transformation
  onBeforeActionResponse: <Out>(c: unknown, name: string, args: unknown[], output: Out): Out => output,

  actions: {},
});
TypeScript

Documentation

Helper Types

Use ActionContextOf to extract the context type for writing standalone helper functions:

import { actor, ActionContextOf } from "rivetkit";

const gameRoom = actor({
  state: { players: [] as string[], score: 0 },
  actions: {
    addPlayer: (c, playerId: string) => {
      validatePlayer(c, playerId);
      c.state.players.push(playerId);
    },
  },
});

// Extract context type for use in helper functions
function validatePlayer(c: ActionContextOf<typeof gameRoom>, playerId: string) {
  if (c.state.players.includes(playerId)) {
    throw new Error("Player already in room");
  }
}
TypeScript

Documentation

Errors

Use UserError to throw errors that are safely returned to clients. Pass metadata to include structured data. Other errors are converted to generic “internal error” for security.

Documentation

Low-Level HTTP & WebSocket Handlers

For custom protocols or integrating libraries that need direct access to HTTP Request/Response or WebSocket connections, use onRequest and onWebSocket.

HTTP Documentation · WebSocket Documentation

Versions & Upgrades

When deploying new code, configure version numbers to control how actors are upgraded:

import { actor, setup } from "rivetkit";

const myActor = actor({ state: {}, actions: {} });

const registry = setup({
  use: { myActor },
  runner: {
    version: 2, // Increment on each deployment
  },
});
TypeScript

Or use environment variable: RIVET_RUNNER_VERSION=2

Common version sources:

  • Build timestamp: Date.now()
  • Git commit count: git rev-list --count HEAD
  • CI build number: github.run_number, GITHUB_RUN_NUMBER, etc.

Documentation

JavaScript Client Quick Reference

Stateless vs Stateful

import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const counter = actor({
  state: { count: 0 },
  actions: {
    increment: (c, amount: number) => {
      c.state.count += amount;
      c.broadcast("count", c.state.count);
      return c.state.count;
    }
  }
});

const registry = setup({ use: { counter } });
const client = createClient<typeof registry>();
const counterHandle = client.counter.getOrCreate(["my-counter"]);

// Stateless: each call is independent, no persistent connection
await counterHandle.increment(1);

// Stateful: persistent connection for realtime events
const conn = counterHandle.connect();
conn.on("count", (value: number) => console.log(value));
await conn.increment(1);
TypeScript

Documentation

Getting Actors

import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const chatRoom = actor({
  state: { messages: [] as string[] },
  actions: {}
});

const game = actor({
  state: { mode: "" },
  createState: (c, input: { mode: string }) => ({ mode: input.mode }),
  actions: {}
});

const registry = setup({ use: { chatRoom, game } });
const client = createClient<typeof registry>();

// Get or create by key
const room = client.chatRoom.getOrCreate(["room-42"]);

// Get existing (returns null if not found)
const existing = client.chatRoom.get(["room-42"]);

// Create with input
const gameHandle = client.game.create(["game-1"], { input: { mode: "ranked" } });
TypeScript

Documentation

Connection Parameters

Pass parameters when connecting to actors for authentication or configuration:

import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

interface ConnParams { authToken: string; }
interface ConnState { userId: string; }

const chatRoom = actor({
  state: { messages: [] as string[] },
  createConnState: (c, params: ConnParams): ConnState => {
    // Validate token and extract user info
    return { userId: "user-123" };
  },
  actions: {}
});

const registry = setup({ use: { chatRoom } });
const client = createClient<typeof registry>();

// Pass params when getting actor handle
const room = client.chatRoom.getOrCreate(["general"], {
  params: { authToken: "jwt-token-here" }
});

// Params are used on connect()
const conn = room.connect();
TypeScript

Documentation

Subscribing to Events

import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const chatRoom = actor({
  state: { messages: [] as string[] },
  actions: {}
});

const registry = setup({ use: { chatRoom } });
const client = createClient<typeof registry>();

const conn = client.chatRoom.getOrCreate(["general"]).connect();
conn.on("message", (msg: string) => console.log(msg));
conn.once("gameOver", () => console.log("done"));
TypeScript

Documentation

Connection Lifecycle

import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const chatRoom = actor({
  state: { messages: [] as string[] },
  actions: {}
});

const registry = setup({ use: { chatRoom } });
const client = createClient<typeof registry>();

const handle = client.chatRoom.getOrCreate(["general"]);

// Resolve actor ID before connecting
const actorId = await handle.resolve();

const conn = handle.connect();

// Lifecycle callbacks
conn.onOpen(() => console.log("connected"));
conn.onClose(() => console.log("disconnected"));
conn.onError((err) => console.error("error:", err));
conn.onStatusChange((status) => console.log("status:", status));

// Cleanup
conn.dispose();
TypeScript

Documentation

Calling from Backend

Call actors from your server-side code.

import { Hono } from "hono";
import { actor, setup } from "rivetkit";
import { createClient } from "rivetkit/client";

const counter = actor({
  state: { count: 0 },
  actions: {
    increment: (c, amount: number) => {
      c.state.count += amount;
      return c.state.count;
    }
  }
});

const registry = setup({ use: { counter } });
const client = createClient<typeof registry>();
const app = new Hono();

app.post("/increment/:name", async (c) => {
  const counterHandle = client.counter.getOrCreate([c.req.param("name")]);
  const newCount = await counterHandle.increment(1);
  return c.json({ count: newCount });
});
TypeScript

Documentation

React Quick Reference

Setup

useActor & Calling Actions

Subscribing to Events

Documentation

Authentication & Security

Validate credentials in onBeforeConnect or createConnState. Throw an error to reject the connection. Use c.conn.id or c.conn.state to identify users in actions—never trust user IDs passed as action parameters.

import { actor, UserError } from "rivetkit";

// Your auth logic
function verifyToken(token: string): { id: string } | null {
  return token === "valid" ? { id: "user123" } : null;
}

const chatRoom = actor({
  state: { messages: [] as string[] },

  createConnState: (_c, params: { token: string }) => {
    const user = verifyToken(params.token);
    if (!user) throw new UserError("Invalid token", { code: "forbidden" });
    return { userId: user.id };
  },

  actions: {
    send: (c, text: string) => {
      // Use c.conn.state for secure identity, not action parameters
      const connState = c.conn.state as { userId: string };
      c.state.messages.push(`${connState.userId}: ${text}`);
    },
  },
});
TypeScript

Documentation

CORS (Cross-Origin Resource Sharing)

Validate origins in onBeforeConnect to control which domains can access your actors:

import { actor, UserError } from "rivetkit";

const myActor = actor({
  state: { count: 0 },
  onBeforeConnect: (c) => {
    const origin = c.request?.headers.get("origin");
    if (origin !== "https://myapp.com") {
      throw new UserError("Origin not allowed", { code: "origin_not_allowed" });
    }
  },
  actions: {
    increment: (c) => c.state.count++,
  },
});
TypeScript

Documentation