Loading...
Loading...
RivetKit backend and Rivet Actor runtime guidance. Use for building, modifying, debugging, or testing Rivet Actors, registries, serverless/runner modes, deployment, or actor-based workflows.
npx skill4agent add rivet-dev/skills rivetkit> Canonical URL: https://rivet.dev/docs/actors/actionshttps://rivet.dev/docs/actors/actionshttps://rivet.dev/docs/clients/reacthttps://rivet.dev/docs/self-hosting/kubernetesnpm install rivetkit@2.0.42setup({ use: { /* actors */ } })registry.serve()registry.handler()registry.startRunner()/api/rivet/metadataimport { createClient } from "rivetkit/client";
import type { registry } from "./actors";
const client = createClient<typeof registry>();
// Single key: one actor per user
client.user.getOrCreate(["user-123"]);
// Compound key: document scoped to an organization
client.document.getOrCreate(["org-acme", "doc-456"]);import { actor, setup } from "rivetkit";
export const user = actor({
state: { name: "" },
actions: {},
});
export const document = actor({
state: { content: "" },
actions: {},
});
export const registry = setup({ use: { user, document } });import { actor, setup } from "rivetkit";
// Coordinator: tracks chat rooms within an organization
export const chatRoomList = actor({
state: { rooms: [] as string[] },
actions: {
addRoom: async (c, name: string) => {
// Create the chat room actor
const client = c.client<typeof registry>();
await client.chatRoom.create([c.key[0], name]);
c.state.rooms.push(name);
},
listRooms: (c) => c.state.rooms,
},
});
// Data actor: handles a single chat room
export const chatRoom = actor({
state: { messages: [] as string[] },
actions: {
send: (c, msg: string) => { c.state.messages.push(msg); },
},
});
export const registry = setup({ use: { chatRoomList, chatRoom } });import { createClient } from "rivetkit/client";
import type { registry } from "./actors";
const client = createClient<typeof registry>();
// Coordinator per org
const coordinator = client.chatRoomList.getOrCreate(["org-acme"]);
await coordinator.addRoom("general");
await coordinator.addRoom("random");
// Access chat rooms created by coordinator
client.chatRoom.get(["org-acme", "general"]);import { createClient } from "rivetkit/client";
import type { registry } from "./actors";
const client = createClient<typeof registry>();
// Shard by hour
const hour = new Date().toISOString().slice(0, 13); // "2024-01-15T09"
client.analytics.getOrCreate(["org-acme", hour]);
// Shard randomly across 3 actors
client.rateLimiter.getOrCreate([`shard-${Math.floor(Math.random() * 3)}`]);import { actor, setup } from "rivetkit";
export const analytics = actor({
state: { events: [] as string[] },
actions: {},
});
export const rateLimiter = actor({
state: { requests: 0 },
actions: {},
});
export const registry = setup({ use: { analytics, rateLimiter } });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 } });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 },
});import { actor, setup } from "rivetkit";
const counter = actor({
state: { count: 0 },
actions: { increment: (c, amount: number) => c.state.count += amount }
});
const registry = setup({ use: { counter } });
// Exposes Rivet API on /api/rivet/ to communicate with actors
export default registry.serve();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 }
});
const registry = setup({ use: { counter } });
// Build client to communicate with actors (optional)
const client = createClient<typeof registry>();
const app = new Hono();
// Exposes Rivet API to communicate with actors
app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
export default app;import { Elysia } from "elysia";
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 } });
// Build client to communicate with actors (optional)
const client = createClient<typeof registry>();
const app = new Elysia()
// Exposes Rivet API to communicate with actors
.all("/api/rivet/*", (c) => registry.handler(c.request));
export default app;
import { actor } from "rivetkit";
const counter = actor({
state: { count: 0 },
actions: {
increment: (c) => c.state.count += 1,
},
});import { actor } from "rivetkit";
interface CounterState {
count: number;
}
const counter = actor({
state: { count: 0 } as CounterState,
createState: (c, input: { start?: number }): CounterState => ({
count: input.start ?? 0,
}),
actions: {
increment: (c) => c.state.count += 1,
},
});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"org:${userId}"userIdimport { 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" }
});import { actor } from "rivetkit";
const counter = actor({
state: { count: 0 },
vars: { lastAccess: 0 },
actions: {
increment: (c) => {
c.vars.lastAccess = Date.now();
return c.state.count += 1;
},
},
});import { actor } from "rivetkit";
const counter = actor({
state: { count: 0 },
createVars: () => ({
emitter: new EventTarget(),
}),
actions: {
increment: (c) => {
c.vars.emitter.dispatchEvent(new Event("change"));
return c.state.count += 1;
},
},
});import { actor } from "rivetkit";
const counter = actor({
state: { count: 0 },
actions: {
increment: (c, amount: number) => (c.state.count += amount),
getCount: (c) => c.state.count,
},
});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 });
},
},
});c.connc.connsc.conn.idc.conn.stateconnStatecreateConnStateimport { actor } from "rivetkit";
const chatRoom = actor({
state: {},
connState: { visitorId: 0 },
onConnect: (c, conn) => {
conn.state.visitorId = Math.random();
},
actions: {
whoAmI: (c) => c.conn.state.visitorId,
},
});import { actor } from "rivetkit";
const chatRoom = actor({
state: {},
// params passed from client
createConnState: (c, params: { userId: string }) => ({
userId: params.userId,
}),
actions: {
// Access current connection's state and params
whoAmI: (c) => ({
state: c.conn.state,
params: c.conn.params,
}),
// Iterate all connections with c.conns
notifyOthers: (c, text: string) => {
for (const conn of c.conns.values()) {
if (conn !== c.conn) conn.send("notification", { text });
}
},
},
});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 } });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 });
},
},
});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();
},
},
});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: {},
});ActionContextOfimport { 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");
}
}UserErrormetadataimport { actor, UserError } from "rivetkit";
const user = actor({
state: { username: "" },
actions: {
updateUsername: (c, username: string) => {
if (username.length < 3) {
throw new UserError("Username too short", {
code: "username_too_short",
metadata: { minLength: 3, actual: username.length },
});
}
c.state.username = username;
},
},
});import { actor, setup } from "rivetkit";
import { createClient, ActorError } from "rivetkit/client";
const user = actor({
state: { username: "" },
actions: { updateUsername: (c, username: string) => { c.state.username = username; } }
});
const registry = setup({ use: { user } });
const client = createClient<typeof registry>();
try {
await client.user.getOrCreate([]).updateUsername("ab");
} catch (error) {
if (error instanceof ActorError) {
console.log(error.code); // "username_too_short"
console.log(error.metadata); // { minLength: 3, actual: 2 }
}
}RequestResponseonRequestonWebSocketimport { actor, setup } from "rivetkit";
export const api = actor({
state: { count: 0 },
onRequest: (c, request) => {
if (request.method === "POST") c.state.count++;
return Response.json(c.state);
},
actions: {},
});
export const registry = setup({ use: { api } });import { createClient } from "rivetkit/client";
import type { registry } from "./registry";
const client = createClient<typeof registry>();
const actor = client.api.getOrCreate(["my-actor"]);
// Use built-in fetch method
const response = await actor.fetch("/count");
// Or get raw URL for external tools
const url = await actor.getGatewayUrl();
const nativeResponse = await fetch(`${url}/request/count`);import { actor, setup } from "rivetkit";
export const chat = actor({
state: { messages: [] as string[] },
onWebSocket: (c, websocket) => {
websocket.addEventListener("open", () => {
websocket.send(JSON.stringify({ type: "history", messages: c.state.messages }));
});
websocket.addEventListener("message", (event) => {
c.state.messages.push(event.data as string);
websocket.send(event.data as string);
c.saveState({ immediate: true });
});
},
actions: {},
});
export const registry = setup({ use: { chat } });import { createClient } from "rivetkit/client";
import type { registry } from "./registry";
const client = createClient<typeof registry>();
const actor = client.chat.getOrCreate(["my-chat"]);
// Use built-in websocket method
const ws = await actor.websocket("/");
// Or get raw URL for external tools
const url = await actor.getGatewayUrl();
const nativeWs = new WebSocket(`${url.replace("http://", "ws://").replace("https://", "wss://")}/websocket/`);import { actor, setup } from "rivetkit";
const myActor = actor({ state: {}, actions: {} });
const registry = setup({
use: { myActor },
runner: {
version: 2, // Increment on each deployment
},
});RIVET_RUNNER_VERSION=2Date.now()git rev-list --count HEADgithub.run_numberGITHUB_RUN_NUMBERonBeforeConnectcreateConnStatec.conn.idc.conn.stateimport { 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}`);
},
},
});onBeforeConnectimport { 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++,
},
});openapi.json