Connect
Open a persistent WebSocket connection to an actor. One connection carries action calls, event subscriptions, and errors as JSON messages, which is what handle.connect() does in the RivetKit client.
{actor} accepts the same forms as every gateway endpoint: an actor ID, {actor_id}@{token}, or an actor name with rvt-* query parameters. See Actor Routing.
Connection options travel as WebSocket subprotocols in the Sec-WebSocket-Protocol header because browsers cannot set arbitrary headers on an upgrade. Always include rivet, then add any of the prefixed entries below.
| Subprotocol | Purpose |
|---|---|
rivet | Required. Lets servers that insist on a matching protocol accept the upgrade. |
rivet_encoding.json | Message encoding. json is the default and is documented here. The RivetKit client uses cbor or bare; the payloads are the same, only the framing differs. |
rivet_token.{token} | Token to authenticate with. See WebSocket Requests. |
rivet_conn_params.{params} | Optional connection parameters passed to the actor’s createConnState and onBeforeConnect hooks, as URL-encoded JSON. |
rivet_skip_ready_wait | Complete the upgrade as soon as the actor is resolved instead of waiting for it to be ready. |
Every message in both directions is a JSON object of the form { "body": { "tag": "<Message>", "val": { ... } } }. The server sends Init first; nothing else arrives before it. There is no ping or heartbeat message. Rely on WebSocket ping frames or your own timeout to detect a dead connection.
GET wss://api.rivet.dev/gateway/{actor}/connect
Handshake
After the upgrade completes, the server sends Init before any other message. Store connectionId if you need to correlate connection-scoped state; nothing else needs to be sent before calling actions or subscribing to events.
// Open a connection by actor name. The selector query parameters are the same
// ones used by the HTTP endpoints; the token and encoding travel as
// WebSocket subprotocols because browsers cannot set headers on an upgrade.
const url = new URL("wss://api.rivet.dev/gateway/counter/connect");
url.searchParams.set("rvt-namespace", process.env.RIVET_NAMESPACE!);
url.searchParams.set("rvt-method", "getOrCreate");
url.searchParams.set("rvt-key", "my-counter");
url.searchParams.set("rvt-pool", "default");
const ws = new WebSocket(url, [
"rivet",
"rivet_encoding.json",
`rivet_token.${process.env.RIVET_TOKEN}`,
]);
// The server sends Init as the first message once the actor is ready.
ws.onmessage = (event) => {
const { body } = JSON.parse(String(event.data));
if (body.tag === "Init") {
console.log(
"connected to actor",
body.val.actorId,
"as",
body.val.connectionId,
);
}
};
export {};
import { createClient } from "rivetkit/client";
import type { registry } from "./index";
const client = createClient<typeof registry>(process.env.RIVET_ENDPOINT);
// connect() opens a WebSocket to /gateway/{actor}/connect and completes the
// Init handshake. The connection reconnects automatically if it drops.
const counter = client.counter.getOrCreate(["my-counter"]);
const conn = counter.connect();
// Close the socket when you are done with it.
await conn.dispose();
Init
Server to client. Initial connection message sent from server to client
| Field | Type | Required | Description |
|---|---|---|---|
actorId | string | Yes | ID of the actor this connection is attached to. |
connectionId | string | Yes | ID of this connection. Matches c.conn.id inside the actor. |
{
"body": {
"tag": "Init",
"val": {
"actorId": "00000000-0000-0000-0000-000000000000",
"connectionId": "00000000-0000-0000-0000-000000000000"
}
}
}
Actions
Send an ActionRequest with a client-chosen id. The server answers with an ActionResponse carrying the same id, or with an Error whose actionId matches. Requests may be pipelined; responses are not guaranteed to arrive in order, so match on id rather than position.
This is the connection equivalent of Call Action over HTTP. Actions called on a connection can also read connection state set in createConnState.
const ws = new WebSocket(
`wss://api.rivet.dev/gateway/${process.env.ACTOR_ID}/connect`,
["rivet", "rivet_encoding.json", `rivet_token.${process.env.RIVET_TOKEN}`],
);
// Pick a unique id per request. The ActionResponse (or Error) that answers
// it carries the same id, so several actions can be in flight at once.
let nextId = 1;
const pending = new Map<number, (output: unknown) => void>();
function callAction(name: string, args: unknown[]): Promise<unknown> {
const id = nextId++;
ws.send(
JSON.stringify({
body: { tag: "ActionRequest", val: { id, name, args } },
}),
);
return new Promise((resolve) => pending.set(id, resolve));
}
ws.onmessage = (event) => {
const { body } = JSON.parse(String(event.data));
if (body.tag === "ActionResponse") {
pending.get(body.val.id)?.(body.val.output);
pending.delete(body.val.id);
}
};
ws.onopen = async () => {
const count = await callAction("increment", [1]);
console.log(count); // 1
};
export {};
import { createClient } from "rivetkit/client";
import type { registry } from "./index";
const client = createClient<typeof registry>(process.env.RIVET_ENDPOINT);
const conn = client.counter.getOrCreate(["my-counter"]).connect();
// Actions called on a connection are sent as ActionRequest messages over the
// WebSocket instead of separate HTTP requests. The promise resolves with the
// ActionResponse output.
const count = await conn.increment(1);
console.log(count); // 1
ActionRequest
Client to server. Request to execute an action on the actor
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | Client-chosen identifier used to match the response. Any integer that is unique among in-flight requests on this connection. |
name | string | Yes | Name of the action as defined in the actor’s actions map. |
args | any | Yes | Positional arguments passed to the action, as a JSON array. |
{
"body": {
"tag": "ActionRequest",
"val": {
"id": 1,
"name": "increment",
"args": [
1
]
}
}
}
ActionResponse
Server to client. Response to an action request
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | The id from the matching ActionRequest. |
output | any | Yes | Value returned by the action. null if the action returned undefined. |
{
"body": {
"tag": "ActionResponse",
"val": {
"id": 1,
"output": 1
}
}
}
Events
Send a SubscriptionRequest with subscribe: true to start receiving Event messages for an event name, and subscribe: false to stop. Subscriptions are per connection and are lost when the connection closes, so resubscribe after reconnecting. The server does not acknowledge subscription changes. If the actor rejects a subscription, the connection is closed with the error as the close reason.
Events sent by the actor to a connection that has not subscribed to that event name are dropped.
const ws = new WebSocket(
`wss://api.rivet.dev/gateway/${process.env.ACTOR_ID}/connect`,
["rivet", "rivet_encoding.json", `rivet_token.${process.env.RIVET_TOKEN}`],
);
ws.onopen = () => {
// Subscribe once. The server pushes an Event message every time the actor
// broadcasts or sends countChanged to this connection.
ws.send(
JSON.stringify({
body: {
tag: "SubscriptionRequest",
val: { eventName: "countChanged", subscribe: true },
},
}),
);
};
ws.onmessage = (event) => {
const { body } = JSON.parse(String(event.data));
if (body.tag === "Event" && body.val.name === "countChanged") {
const [count] = body.val.args;
console.log("count is now", count);
}
};
export {};
import { createClient } from "rivetkit/client";
import type { registry } from "./index";
const client = createClient<typeof registry>(process.env.RIVET_ENDPOINT);
const conn = client.counter.getOrCreate(["my-counter"]).connect();
// on() sends a SubscriptionRequest and routes matching Event messages to the
// callback. The returned function unsubscribes.
const unsubscribe = conn.on("countChanged", (count: number) => {
console.log("count is now", count);
});
await conn.increment(1); // logs "count is now 1"
unsubscribe();
SubscriptionRequest
Client to server. Request to subscribe or unsubscribe from an event
| Field | Type | Required | Description |
|---|---|---|---|
eventName | string | Yes | Name of the event as defined in the actor’s events map. |
subscribe | boolean | Yes | true to subscribe, false to unsubscribe. |
{
"body": {
"tag": "SubscriptionRequest",
"val": {
"eventName": "countChanged",
"subscribe": true
}
}
}
Event
Server to client. Event broadcast to subscribed clients
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Name of the event. |
args | any | Yes | Arguments passed to c.broadcast() or conn.send(), as a JSON array. |
{
"body": {
"tag": "Event",
"val": {
"name": "countChanged",
"args": [
1
]
}
}
}
Errors
Errors on an established connection arrive as Error messages. actionId is set when the error answers a specific ActionRequest and null for connection-level errors such as a malformed message. The group and code values are the same ones the HTTP API returns; see Error Codes.
Errors that prevent the connection from being established, such as an invalid token or an actor that does not exist, are reported by closing the WebSocket with a reason of the form {group}.{code}#{ray_id}. See WebSockets.
const ws = new WebSocket(
`wss://api.rivet.dev/gateway/${process.env.ACTOR_ID}/connect`,
["rivet", "rivet_encoding.json", `rivet_token.${process.env.RIVET_TOKEN}`],
);
ws.onmessage = (event) => {
const { body } = JSON.parse(String(event.data));
if (body.tag !== "Error") return;
const { group, code, message, actionId } = body.val;
if (actionId !== null) {
// The error answers the ActionRequest with this id.
console.error(
`action ${actionId} failed: ${group}.${code}: ${message}`,
);
} else {
// Connection-level error, for example a rejected message.
console.error(`connection error: ${group}.${code}: ${message}`);
}
};
// Errors that reject the upgrade itself arrive as a close frame instead,
// with a reason of the form "{group}.{code}#{ray_id}".
ws.onclose = (event) => {
if (event.code !== 1000) console.error("closed:", event.code, event.reason);
};
export {};
import { ActorError, createClient } from "rivetkit/client";
import type { registry } from "./index";
const client = createClient<typeof registry>(process.env.RIVET_ENDPOINT);
const conn = client.counter.getOrCreate(["my-counter"]).connect();
// Error messages that answer an action reject that action's promise.
try {
await conn.increment(1);
} catch (error) {
if (error instanceof ActorError) {
console.error(`${error.group}.${error.code}: ${error.message}`);
}
}
// Errors that are not tied to an action go to onError.
conn.onError((error) => {
console.error(`connection error: ${error.group}.${error.code}`);
});
Error
Server to client. Error message sent from server to client
| Field | Type | Required | Description |
|---|---|---|---|
group | string | Yes | Subsystem that produced the error, such as actor, auth, or user. |
code | string | Yes | Error identifier within the group. |
message | string | Yes | Human-readable explanation. Do not match on it. |
metadata | any | No | Structured details attached by the actor or gateway, if any. |
actionId | integer | any | null | Yes | id of the ActionRequest this error answers, or null for connection-level errors. |
actor | object | No | Actor that handled the message, when known. |
actor.actorId | string | Yes | ID of the actor. |
actor.generation | number | integer | Yes | Generation of the actor process. Increments each time the actor restarts. |
actor.key | string | No | Key of the actor, if it has one. |
{
"body": {
"tag": "Error",
"val": {
"group": "user",
"code": "insufficient_funds",
"message": "Balance is too low.",
"metadata": {
"balance": 5
},
"actionId": 1,
"actor": {
"actorId": "00000000-0000-0000-0000-000000000000",
"generation": 1,
"key": "my-counter"
}
}
}
}