rivetkit by rivet-dev/skills
npx skills add https://github.com/rivet-dev/skills --skill rivetkit在 Rivet 的 Actor 运行时上构建高性能、长生命周期的内存进程。当 HTTP 端点、数据库或队列不再满足 AI 智能体、实时协作、工作流编排或地理分布式数据的需求时,请使用此技能。
如果某些功能未按预期工作或您遇到困难,请提示用户加入 Rivet Discord 或在 Rivet GitHub 上提交问题以报告问题并获取帮助。
向用户提供一份预生成的报告,内容包括:
使用检查器 HTTP API 来检查正在运行的 actors。这些端点可通过网关在 /gateway/{actor_id}/inspector/* 访问。关键端点:
GET /inspector/summary - 完整的 actor 快照(状态、连接、RPC、队列)GET /inspector/state / PATCH /inspector/state - 读取/写入 actor 状态广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
GET /inspector/connections - 活跃连接GET /inspector/rpcs - 可用的操作POST /inspector/action/{name} - 使用 {"args": [...]} 执行一个操作GET /inspector/queue?limit=50 - 队列状态GET /inspector/traces?startMs=0&endMs=...&limit=1000 - 跟踪跨度(OTLP JSON)GET /inspector/workflow-history - 工作流历史记录和状态在本地开发环境中,不需要身份验证令牌。在生产环境中,传递 Authorization: Bearer <RIVET_INSPECTOR_TOKEN>。详情请参阅调试文档。
当提供来自 Rivet 文档的信息时,请引用规范 URL,以便用户可以了解更多信息。每个参考文件在其头部元数据中都包含其规范 URL。
如何引用:
查找规范 URL:
下面的参考地图链接到参考文件。每个文件的头部包含:
> Canonical URL: https://rivet.dev/docs/actors/actions
引用时请使用该规范 URL,而不是参考文件路径。
示例:
https://rivet.dev/docs/actors/actionshttps://rivet.dev/docs/clients/reacthttps://rivet.dev/docs/self-hosting/kubernetes安装 RivetKit(最新版本:2.1.6)
npm install rivetkit@2.1.6
使用 setup({ use: { /* actors */ } }) 定义一个注册表。
暴露 registry.serve() 或 registry.handler()(无服务器模式)或 registry.startRunner()(运行器模式)。除非用户有特定理由使用运行器模式,否则优先选择无服务器模式。
在部署前验证 /api/rivet/metadata 返回 200。
配置 Rivet Cloud 或自托管引擎
集成客户端(请参阅下面的 JavaScript、React 或 Swift 客户端指南)
提示用户是否要部署。如果是,请转到"部署 Rivet 后端"。
有关更多信息,请阅读与用户项目相关的快速入门指南。
try/catch。catch,则必须显式处理错误,至少通过记录它。c.vars 是临时的。 c.vars 中的数据在每次重启、崩溃、升级或休眠/唤醒周期中都会丢失。仅将 c.vars 用于不可序列化的对象(例如,物理引擎、WebSocket 引用、事件发射器、缓存)或真正瞬时的运行时数据(例如,断开连接后无关紧要的当前输入方向)。
持久化存储选项。 任何必须在重启后保留的数据都应属于以下选项之一,而不是 c.vars:
c.state — 适用于小型、有界数据集的 CBOR 可序列化数据。非常适合配置、计数器、小型玩家列表、阶段标志等。保持在 128 KB 以下。不要在此处存储无界或增长的数据(例如,聊天日志、事件历史记录、无限增长的生成实体列表)。状态在每次持久化周期中作为单个块读取/写入。c.kv — 用于无界数据的键值存储。这是 c.state 底层使用的机制。支持二进制值。用于较大或可变大小的数据,如用户库存、世界区块、文件块,或任何可能随时间增长的集合。键的作用域限定在 actor 实例内。c.db — 用于结构化或复杂数据的 SQLite 数据库。当您需要查询、索引、连接、聚合或关系建模时使用。非常适合排行榜、比赛历史记录、玩家池或任何受益于 SQL 的数据。常见错误: 将重要的游戏/应用程序数据存储在 c.vars 中而不是持久化它。例如,如果用户可以在物理模拟中生成对象,则生成定义(位置、大小、类型)必须持久化在 c.state 中(如果无界,则在 c.kv 中),即使物理引擎句柄(不可序列化)位于 c.vars 中。重启时,run() 应从持久化数据中重新创建运行时对象。
除非另有说明,否则假设用户正在部署到 Rivet Cloud。如果用户是自托管,请阅读下面的自托管指南。
RivetKit OpenAPI 规范可在技能目录的 openapi.json 文件中找到。该文件记录了用于管理 actors 的所有 HTTP 端点。
c.client<typeof registry>() 时,双向 actor 调用可能会创建循环类型依赖。c.state 变为 unknown,actor 方法可能变为 undefined,或者在第一次跨 actor 调用后出现 TS2322 / TS2722 错误。c.client<typeof registry>() 的推断。unknown,并明确说明这将在该调用点放弃类型安全性。actors.ts
import { actor, event, setup } from "rivetkit";
const counter = actor({
state: { count: 0 },
events: {
count: event<number>(),
},
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 },
});
server.ts
如果适用,与用户现有的服务器集成。否则,默认为 Hono。
import { registry } from "./actors";
export default registry.serve();
import { Hono } from "hono";
import { registry } from "./actors";
const app = new Hono();
app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
export default app;
import { Elysia } from "elysia";
import { registry } from "./actors";
const app = new Elysia()
.all("/api/rivet/*", (c) => registry.handler(c.request));
export default app;
使用与您的应用匹配的客户端 SDK:
在重启、崩溃和部署后仍然存在的持久化数据。状态会持久化到 Rivet Cloud 或 Rivet 自托管环境,因此如果当前进程崩溃或退出,状态可以在重启后存活。
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,
},
});
键唯一标识 actor 实例。使用复合键(数组)进行分层寻址:
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>();
// 复合键: [org, room]
client.chatRoom.getOrCreate(["org-acme", "general"]);
// 通过 c.key 在 actor 内部访问键
当 userId 包含用户数据时,不要使用字符串插值如 "org:${userId}" 来构建键。请改用数组以防止键注入攻击。
在创建 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>();
// 客户端用法
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;
},
},
});
操作是客户端和其他 actors 与 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,
},
});
事件支持从 actors 到已连接客户端的实时通信。
import { actor, event } from "rivetkit";
const chatRoom = actor({
state: { messages: [] as string[] },
events: {
newMessage: event<{ text: string }>(),
},
actions: {
sendMessage: (c, text: string) => {
// 广播给所有已连接的客户端
c.broadcast("newMessage", { text });
},
},
});
通过 c.conn 访问当前连接,或通过 c.conns 访问所有已连接的客户端。使用 c.conn.id 或 c.conn.state 来安全地识别谁正在调用一个操作。连接状态通过 connState 或 createConnState 初始化,后者接收客户端在连接时传递的参数。
import { 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: {},
// 从客户端传递的参数
createConnState: (c, params: { userId: string }) => ({
userId: params.userId,
}),
actions: {
// 访问当前连接的状态和参数
whoAmI: (c) => ({
state: c.conn.state,
params: c.conn.params,
}),
// 使用 c.conns 遍历所有连接
notifyOthers: (c, text: string) => {
for (const conn of c.conns.values()) {
if (conn !== c.conn) conn.send("notification", { text });
}
},
},
});
使用队列在 run 循环中按顺序处理持久化消息。
import { actor, queue } from "rivetkit";
const counter = actor({
state: { value: 0 },
queues: {
increment: queue<{ amount: number }>(),
},
run: async (c) => {
for await (const message of c.queue.iter()) {
c.state.value += message.body.amount;
}
},
});
当您的 run 逻辑需要持久化、可重放的多步骤执行时,请使用工作流。
import { actor, queue } from "rivetkit";
import { workflow } from "rivetkit/workflow";
const worker = actor({
state: { processed: 0 },
queues: {
tasks: queue<{ url: string }>(),
},
run: workflow(async (ctx) => {
await ctx.loop("task-loop", async (loopCtx) => {
const message = await loopCtx.queue.next("wait-task");
await loopCtx.step("process-task", async () => {
await processTask(message.body.url);
loopCtx.state.processed += 1;
});
});
}),
});
async function processTask(url: string): Promise<void> {
const res = await fetch(url, { method: "POST" });
if (!res.ok) throw new Error(`Task failed: ${res.status}`);
}
Actors 可以使用 c.client() 调用其他 actors。
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, event } from "rivetkit";
const reminder = actor({
state: { message: "" },
events: {
reminder: event<{ message: string }>(),
},
actions: {
// 安排操作在延迟(毫秒)后运行
setReminder: (c, message: string, delayMs: number) => {
c.state.message = message;
c.schedule.after(delayMs, "sendReminder");
},
// 安排操作在特定时间戳运行
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() 永久删除一个 actor 及其状态。
import { actor } from "rivetkit";
const userAccount = actor({
state: { email: "", name: "" },
onDestroy: (c) => {
console.log(`Account ${c.state.email} deleted`);
},
actions: {
deleteAccount: (c) => {
c.destroy();
},
},
});
Actors 支持用于初始化、后台处理、连接、网络和状态更改的钩子。使用 run 进行长生命周期的后台循环,并在关闭时使用 c.aborted 或 c.abortSignal 干净地退出。
import { actor, event, queue } 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,
events: {
stateChanged: event<RoomState>(),
},
queues: {
work: queue<{ task: string }>(),
},
// 状态和变量初始化
createState: (c, input: RoomInput): RoomState => ({ users: {}, name: input.roomName }),
createVars: () => ({ startTime: Date.now() }),
// Actor 生命周期
onCreate: (c) => console.log("created", c.key),
onDestroy: (c) => console.log("destroyed"),
onWake: (c) => console.log("actor started"),
onSleep: (c) => console.log("actor sleeping"),
run: async (c) => {
for await (const message of c.queue.iter()) {
console.log("processing", message.body.task);
}
},
onStateChange: (c, newState) => c.broadcast("stateChanged", newState),
// 连接生命周期
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),
// 网络
onRequest: (c, req) => new Response(JSON.stringify(c.state)),
onWebSocket: (c, socket) => socket.addEventListener("message", console.log),
// 响应转换
onBeforeActionResponse: <Out>(c: unknown, name: string, args: unknown[], output: Out): Out => output,
actions: {},
});
在 actor 定义之外编写辅助函数时,使用 *ContextOf<typeof myActor> 来提取正确的上下文类型。不要手动定义自己的上下文接口——始终从 actor 定义派生它。
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);
},
},
});
// 正确:从 actor 定义派生上下文类型
function validatePlayer(c: ActionContextOf<typeof gameRoom>, playerId: string) {
if (c.state.players.includes(playerId)) {
throw new Error("Player already in room");
}
}
// 错误:不要像这样手动定义上下文类型
// type MyContext = { state: { players: string[] }; ... };
使用 UserError 抛出安全返回给客户端的错误。传递 metadata 以包含结构化数据。其他错误会转换为通用的"内部错误"以确保安全。
import { 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 }
}
}
对于自定义协议或需要直接访问 HTTP Request/Response 或 WebSocket 连接的库集成,请使用 onRequest 和 onWebSocket。
通过显示名称和图标自定义 actors 在 UI 中的显示方式。建议始终为 actors 提供名称和图标,以便在仪表板中更容易区分它们。
import { actor } from "rivetkit";
const chatRoom = actor({
options: {
name: "Chat Room",
icon: "💬", // 或 FontAwesome: "comments", "chart-line", 等。
},
// ...
});
完整的客户端指南请参见:
Actors 通过隔离状态和消息传递自然地扩展。使用以下模式构建您的应用程序:
为每个用户、文档或房间创建一个 actor。使用复合键来限定实体的作用域:
import { createClient } from "rivetkit/client";
import type { registry } from "./actors";
const client = createClient<typeof registry>();
// 单个键:每个用户一个 actor
client.user.getOrCreate(["user-123"]);
// 复合键:限定在组织内的文档
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 } });
数据 actors 处理核心逻辑(聊天室、游戏会话、用户数据)。协调器 actors 跟踪和管理数据 actors 的集合——将它们视为索引。
import { actor, setup } from "rivetkit";
// 协调器:跟踪组织内的聊天室
export const chatRoomList = actor({
state: { rooms: [] as string[] },
actions: {
addRoom: async (c, name: string) => {
// 创建聊天室 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,
},
});
// 数据 actor:处理单个聊天室
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>();
// 每个组织的协调器
const coordinator = client.chatRoomList.getOrCreate(["org-acme"]);
await coordinator.addRoom("general");
await coordinator.addRoom("random");
// 访问由协调器创建的聊天室
client.chatRoom.get(["org-acme", "general"]);
在 actor 内部使用 run 循环进行连续的后台工作。按顺序处理队列消息、按间隔运行逻辑、流式传输 AI 响应或协调长时间运行的任务。
import { actor, queue, setup } from "rivetkit";
const counterWorker = actor({
state: { value: 0 },
queues: {
mutate: queue<{ delta: number }>(),
},
run: async (c) => {
for await (const message of c.queue.iter()) {
c.state.value += message.body.delta;
}
},
actions: {
getValue: (c) => c.state.value,
},
});
const registry = setup({ use: { counterWorker } });
使用此模式处理长生命周期、持久化的工作流,这些工作流初始化资源、在循环中处理命令,然后进行清理。
import { actor, queue, setup } from "rivetkit";
import { Loop, workflow } from "rivetkit/workflow";
type WorkMessage = { amount: number };
type ControlMessage = { type: "stop"; reason: string };
const worker = actor({
state: {
phase: "idle" as "idle" | "running" | "stopped",
processed: 0,
total: 0,
stopReason: null as string | null,
},
queues: {
work: queue<WorkMessage>(),
control: queue<ControlMessage>(),
},
run: workflow(async (ctx) => {
await ctx.step("setup", async () => {
await fetch("https://api.example.com/workers/init", { method: "POST" });
ctx.state.phase = "running";
ctx.state.stopReason = null;
});
const stopReason = await ctx.loop("worker-loop", async (loopCtx) => {
const message = await loopCtx.queue.next("wait-command", {
names: ["work", "control"],
});
if (message.name === "work") {
await loopCtx.step("apply-work", async () => {
await fetch("https://api.example.com/workers/process", {
method: "POST",
body: JSON.stringify({ amount: message.body.amount }),
});
loopCtx.state.processed += 1;
loopCtx.state.total += message.body.amount;
});
return;
}
return Loop.break((message.body as ControlMessage).reason);
});
await ctx.step("teardown", async () => {
await fetch("https://api.example.com/workers/shutdown", { method: "POST" });
ctx.state.phase = "stopped";
ctx.state.stopReason = stopReason;
});
}),
});
const registry = setup({ use: { worker } });
onBeforeConnect 或 createConnState 中验证凭据,并抛出错误以拒绝未经授权的连接。c.conn.state 来安全地识别用户,而不是信任操作参数。onBeforeConnect 中验证请求来源。部署新代码时,设置一个版本号,以便 Rivet 可以将新的 actors 路由到最新的运行器,并可选择性地排空旧的运行器。使用构建时间戳、git 提交计数或 CI 构建编号作为版本。在部署到生产环境之前,配置版本控制非常重要。没有版本控制,actors 可能会因为运行在较旧的运行器版本上而退化,并且现有的 actors 永远不会被迫迁移到新的运行器。它们将无限期地在旧的运行器上运行,直到退出。
不要将所有逻辑放在一个 actor 中。一个上帝 actor 通过一个瓶颈序列化每个操作,扼杀并行性,并使整个系统作为一个单元失败。按实体拆分为专注的 actors。
Actors 是长生命周期的,并在请求之间维护状态。为每个传入请求创建一个新的 actor 会丢弃该模型的核心优势,并浪费资源在 actor 的创建和销毁上。将 actors 用于持久化实体,将常规函数用于无状态工作。
Build high-performance, long-lived, in-memory processes on Rivet's actor runtime. Use this skill whenever HTTP endpoints, databases, or queues no longer meet the requirements for AI agents, realtime collaboration, workflow orchestration, or geo-distributed data.
If something is not working as intended or you are stuck, prompt the user to join the Rivet Discord or file an issue on the Rivet GitHub to report an issue and get help.
Provide the user with a pre-generated report with:
Use the inspector HTTP API to examine running actors. These endpoints are accessible through the gateway at /gateway/{actor_id}/inspector/*. Key endpoints:
GET /inspector/summary - full actor snapshot (state, connections, RPCs, queue)GET /inspector/state / PATCH /inspector/state - read/write actor stateGET /inspector/connections - active connectionsGET /inspector/rpcs - available actionsPOST /inspector/action/{name} - execute an action with {"args": [...]}GET /inspector/queue?limit=50 - queue statusGET /inspector/traces?startMs=0&endMs=...&limit=1000 - trace spans (OTLP JSON)GET /inspector/workflow-history - workflow history and statusIn local dev, no auth token is needed. In production, pass Authorization: Bearer <RIVET_INSPECTOR_TOKEN>. See the debugging docs for details.
When providing information from Rivet documentation, cite the canonical URL so users can learn more. Each reference file includes its canonical URL in the header metadata.
How to cite:
Finding canonical URLs:
The Reference Map below links to reference files. Each file's header contains:
> Canonical URL: https://rivet.dev/docs/actors/actions
Use that canonical URL when citing, not the reference file path.
Examples:
https://rivet.dev/docs/actors/actionshttps://rivet.dev/docs/clients/reacthttps://rivet.dev/docs/self-hosting/kubernetesInstall RivetKit (latest: 2.1.6)
npm install rivetkit@2.1.6
Define a registry with setup({ use: { /* actors */ } }).
Expose registry.serve() or registry.handler() (serverless) or registry.startRunner() (runner mode). Prefer serverless mode unless the user has a specific reason to use runner mode.
Verify /api/rivet/metadata returns 200 before deploying.
Configure Rivet Cloud or self-hosted engine
Integrate clients (see client guides below for JavaScript, React, or Swift)
Prompt the user if they want to deploy. If so, go to Deploying Rivet Backends.
For more information, read the quickstart guide relevant to the user's project.
try/catch unless it is required for a real recovery path, cleanup boundary, or to add actionable context.catch, you must handle the error explicitly, at minimum by logging it.c.vars is ephemeral. Data in c.vars is lost on every restart, crash, upgrade, or sleep/wake cycle. Only use c.vars for non-serializable objects (e.g., physics engines, WebSocket references, event emitters, caches) or truly transient runtime data (e.g., current input direction that doesn't matter after disconnect).
Persistent storage options. Any data that must survive restarts belongs in one of these, NOT in c.vars:
c.state — CBOR-serializable data for small, bounded datasets. Ideal for configuration, counters, small player lists, phase flags, etc. Keep under 128 KB. Do not store unbounded or growing data here (e.g., chat logs, event histories, spawned entity lists that grow without limit). State is read/written as a single blob on every persistence cycle.c.kv — Key-value store for unbounded data. This is what c.state uses under the hood. Supports binary values. Use for larger or variable-size data like user inventories, world chunks, file blobs, or any collection that may grow over time. Keys are scoped to the actor instance.c.db — SQLite database for structured or complex data. Use when you need queries, indexes, joins, aggregations, or relational modeling. Ideal for leaderboards, match histories, player pools, or any data that benefits from SQL.Common mistake: Storing meaningful game/application data in c.vars instead of persisting it. For example, if users can spawn objects in a physics simulation, the spawn definitions (position, size, type) must be persisted in c.state (or c.kv if unbounded), even though the physics engine handles (non-serializable) live in c.vars. On restart, run() should recreate the runtime objects from the persisted data.
Assume the user is deploying to Rivet Cloud, unless otherwise specified. If user is self-hosting, read the self-hosting guides below.
The RivetKit OpenAPI specification is available in the skill directory at openapi.json. This file documents all HTTP endpoints for managing actors.
c.client<typeof registry>().c.state becoming unknown, actor methods becoming possibly undefined, or TS2322 / TS2722 errors after the first cross-actor call.c.client<typeof registry>().unknown for the registry type and be explicit that this gives up type safety at that call site.actors.ts
import { actor, event, setup } from "rivetkit";
const counter = actor({
state: { count: 0 },
events: {
count: event<number>(),
},
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 },
});
server.ts
Integrate with the user's existing server if applicable. Otherwise, default to Hono.
import { registry } from "./actors";
export default registry.serve();
import { Hono } from "hono";
import { registry } from "./actors";
const app = new Hono();
app.all("/api/rivet/*", (c) => registry.handler(c.req.raw));
export default app;
import { Elysia } from "elysia";
import { registry } from "./actors";
const app = new Elysia()
.all("/api/rivet/*", (c) => registry.handler(c.request));
export default app;
Use the client SDK that matches your app:
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.
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,
},
});
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
Don't build keys with string interpolation like "org:${userId}" when userId contains user data. Use arrays instead to prevent key injection attacks.
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" }
});
Temporary data that doesn't survive restarts. Use for non-serializable objects (event emitters, connections, etc).
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;
},
},
});
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,
},
});
Events enable real-time communication from actors to connected clients.
import { actor, event } from "rivetkit";
const chatRoom = actor({
state: { messages: [] as string[] },
events: {
newMessage: event<{ text: string }>(),
},
actions: {
sendMessage: (c, text: string) => {
// Broadcast to ALL connected clients
c.broadcast("newMessage", { text });
},
},
});
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.
import { 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 });
}
},
},
});
Use queues to process durable messages in order inside a run loop.
import { actor, queue } from "rivetkit";
const counter = actor({
state: { value: 0 },
queues: {
increment: queue<{ amount: number }>(),
},
run: async (c) => {
for await (const message of c.queue.iter()) {
c.state.value += message.body.amount;
}
},
});
Use workflows when your run logic needs durable, replayable multi-step execution.
import { actor, queue } from "rivetkit";
import { workflow } from "rivetkit/workflow";
const worker = actor({
state: { processed: 0 },
queues: {
tasks: queue<{ url: string }>(),
},
run: workflow(async (ctx) => {
await ctx.loop("task-loop", async (loopCtx) => {
const message = await loopCtx.queue.next("wait-task");
await loopCtx.step("process-task", async () => {
await processTask(message.body.url);
loopCtx.state.processed += 1;
});
});
}),
});
async function processTask(url: string): Promise<void> {
const res = await fetch(url, { method: "POST" });
if (!res.ok) throw new Error(`Task failed: ${res.status}`);
}
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 } });
Schedule actions to run after a delay or at a specific time. Schedules persist across restarts, upgrades, and crashes.
import { actor, event } from "rivetkit";
const reminder = actor({
state: { message: "" },
events: {
reminder: event<{ message: string }>(),
},
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 });
},
},
});
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();
},
},
});
Actors support hooks for initialization, background processing, connections, networking, and state changes. Use run for long-lived background loops, and exit cleanly on shutdown with c.aborted or c.abortSignal.
import { actor, event, queue } 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,
events: {
stateChanged: event<RoomState>(),
},
queues: {
work: queue<{ task: string }>(),
},
// 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"),
run: async (c) => {
for await (const message of c.queue.iter()) {
console.log("processing", message.body.task);
}
},
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: {},
});
When writing helper functions outside the actor definition, use *ContextOf<typeof myActor> to extract the correct context type. Do not manually define your own context interface — always derive it from the actor definition.
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);
},
},
});
// Good: derive context type from actor definition
function validatePlayer(c: ActionContextOf<typeof gameRoom>, playerId: string) {
if (c.state.players.includes(playerId)) {
throw new Error("Player already in room");
}
}
// Bad: don't manually define context types like this
// type MyContext = { state: { players: string[] }; ... };
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.
import { 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 }
}
}
For custom protocols or integrating libraries that need direct access to HTTP Request/Response or WebSocket connections, use onRequest and onWebSocket.
HTTP Handler Documentation · WebSocket Handler Documentation
Customize how actors appear in the UI with display names and icons. It's recommended to always provide a name and icon to actors in order to make them easier to distinguish in the dashboard.
import { actor } from "rivetkit";
const chatRoom = actor({
options: {
name: "Chat Room",
icon: "💬", // or FontAwesome: "comments", "chart-line", etc.
},
// ...
});
Find the full client guides here:
Actors scale naturally through isolated state and message-passing. Structure your applications with these patterns:
Create one actor per user, document, or room. Use compound keys to scope entities:
import { 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 } });
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.
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"]);
Use a run loop for continuous background work inside an actor. Process queue messages in order, run logic on intervals, stream AI responses, or coordinate long-running tasks.
import { actor, queue, setup } from "rivetkit";
const counterWorker = actor({
state: { value: 0 },
queues: {
mutate: queue<{ delta: number }>(),
},
run: async (c) => {
for await (const message of c.queue.iter()) {
c.state.value += message.body.delta;
}
},
actions: {
getValue: (c) => c.state.value,
},
});
const registry = setup({ use: { counterWorker } });
Use this pattern for long-lived, durable workflows that initialize resources, process commands in a loop, then clean up.
import { actor, queue, setup } from "rivetkit";
import { Loop, workflow } from "rivetkit/workflow";
type WorkMessage = { amount: number };
type ControlMessage = { type: "stop"; reason: string };
const worker = actor({
state: {
phase: "idle" as "idle" | "running" | "stopped",
processed: 0,
total: 0,
stopReason: null as string | null,
},
queues: {
work: queue<WorkMessage>(),
control: queue<ControlMessage>(),
},
run: workflow(async (ctx) => {
await ctx.step("setup", async () => {
await fetch("https://api.example.com/workers/init", { method: "POST" });
ctx.state.phase = "running";
ctx.state.stopReason = null;
});
const stopReason = await ctx.loop("worker-loop", async (loopCtx) => {
const message = await loopCtx.queue.next("wait-command", {
names: ["work", "control"],
});
if (message.name === "work") {
await loopCtx.step("apply-work", async () => {
await fetch("https://api.example.com/workers/process", {
method: "POST",
body: JSON.stringify({ amount: message.body.amount }),
});
loopCtx.state.processed += 1;
loopCtx.state.total += message.body.amount;
});
return;
}
return Loop.break((message.body as ControlMessage).reason);
});
await ctx.step("teardown", async () => {
await fetch("https://api.example.com/workers/shutdown", { method: "POST" });
ctx.state.phase = "stopped";
ctx.state.stopReason = stopReason;
});
}),
});
const registry = setup({ use: { worker } });
onBeforeConnect or createConnState and throw an error to reject unauthorized connections.c.conn.state to securely identify users in actions rather than trusting action parameters.onBeforeConnect.Authentication Documentation · CORS Documentation
When deploying new code, set a version number so Rivet can route new actors to the latest runner and optionally drain old ones. Use a build timestamp, git commit count, or CI build number as the version. It is very important to configure versioning before deploying to production. Without versioning, actors can regress by running on older runner versions, and existing actors will never be forced to migrate to new runners. They will continue running indefinitely on the old runners until they exit.
Do not put all your logic in a single actor. A god actor serializes every operation through one bottleneck, kills parallelism, and makes the entire system fail as a unit. Split into focused actors per entity.
Actors are long-lived and maintain state across requests. Creating a new actor for every incoming request throws away the core benefit of the model and wastes resources on actor creation and teardown. Use actors for persistent entities and regular functions for stateless work.
Weekly Installs
2.1K
Repository
GitHub Stars
8
First Seen
Jan 26, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
github-copilot2.0K
codex1.3K
opencode1.3K
gemini-cli1.3K
amp1.3K
kimi-cli1.3K
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
102,200 周安装
AI智能体长期记忆系统 - 精英级架构,融合6种方法,永不丢失上下文
1,200 周安装
AI新闻播客制作技能:实时新闻转对话式播客脚本与音频生成
1,200 周安装
Word文档处理器:DOCX创建、编辑、分析与修订痕迹处理全指南 | 自动化办公解决方案
1,200 周安装
React Router 框架模式指南:全栈开发、文件路由、数据加载与渲染策略
1,200 周安装
Nano Banana AI 图像生成工具:使用 Gemini 3 Pro 生成与编辑高分辨率图像
1,200 周安装
SVG Logo Designer - AI 驱动的专业矢量标识设计工具,生成可缩放品牌标识
1,200 周安装