| name | rivetkit-client-rust |
| description | RivetKit Rust client guidance. Use for Rust clients and backends that connect to Rivet Actors with rivetkit::client, create typed actor handles, call actions, or manage connections. |
RivetKit Rust Client
Use this skill when building Rust clients that connect to Rivet Actors with rivetkit::client.
Version
RivetKit version: 2.3.4
First Steps
- Add the dependency
cargo add rivetkit anyhow async-trait
cargo add serde --features derive
cargo add tokio --features full
- Create a client with
Client::new(ClientConfig::new(endpoint)) and call typed actions with get_or_create_typed_default::<A>(...).
Error Handling Policy
- Prefer fail-fast behavior by default. Propagate
anyhow::Result with ?.
- Avoid swallowing errors with broad
match arms unless absolutely needed.
- If an error is handled inline, handle it explicitly, at minimum by logging it.
Rust support is in beta. The supported public Rust API is rivetkit and rivetkit::client; lower-level crates are internal implementation details and do not carry a stability guarantee. See the full API reference on docs.rs/rivetkit, or the runnable hello-world-rust example.
Getting Started
See the Rust quickstart guide for getting started.
Install
Add the rivetkit crate and its companions:
cargo add rivetkit anyhow async-trait
cargo add serde --features derive
cargo add tokio --features full
The Rust client is strongly typed. It shares the same action and event types as your actor, so define your actor in src/lib.rs and import those types from both your server and your client. There is no need to redefine the actor on the client. See Define Your Actor in the quickstart for the actor definition this page builds on.
Minimal Client
use counter::{Counter, Increment};
use rivetkit::{
client::{Client, ClientConfig},
prelude::*,
TypedClientExt,
};
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
let counter = client.get_or_create_typed_default::<Counter>("counter", ["my-counter"])?;
let count = counter.send(Increment { amount: 1 }).await?;
println!("New count: {count}");
Ok(())
}
counter here is your crate name (the package name in Cargo.toml, with dashes as underscores). Counter and Increment are the types you defined alongside your actor.
Stateless vs Stateful
use counter::{Counter, Increment, NewCount};
use rivetkit::{
client::{Client, ClientConfig},
prelude::*,
TypedClientExt,
};
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
let counter = client.get_or_create_typed_default::<Counter>("counter", ["my-counter"])?;
counter.send(Increment { amount: 1 }).await?;
let connection = counter.connect();
connection
.on::<NewCount>(|event| println!("count: {}", event.count))
.await;
connection.send(Increment { amount: 1 }).await?;
connection.disconnect().await;
Ok(())
}
A stateless call on the handle opens a short-lived request per action. A connection keeps a WebSocket open so you can receive events and reuse it across calls.
Getting Actors
use counter::Counter;
use rivetkit::{
client::{Client, ClientConfig, GetOrCreateOptions},
prelude::*,
TypedClientExt,
};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
let room = client.get_or_create_typed_default::<Counter>("counter", ["room-42"])?;
let existing = client.get_typed_default::<Counter>("counter", ["room-42"])?;
let created = client.get_or_create_typed::<Counter>(
"counter",
["game-1"],
GetOrCreateOptions {
create_with_input: Some(json!({ "mode": "ranked" })),
..Default::default()
},
)?;
let by_id = client.get_for_id("counter", "actor-id", Default::default())?;
let resolved_id = room.inner().resolve().await?;
println!("Resolved ID: {resolved_id}");
Ok(())
}
get_typed_default / get_or_create_typed_default use default options. The non-default variants (get_typed / get_or_create_typed) take GetOptions / GetOrCreateOptions for connection parameters, input, and region.
Connection Parameters
Pass connection parameters through the handle options. They are delivered to the actor's create_conn_state callback:
use counter::Counter;
use rivetkit::{
client::{Client, ClientConfig, GetOrCreateOptions},
prelude::*,
TypedClientExt,
};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
let chat = client.get_or_create_typed::<Counter>(
"counter",
["general"],
GetOrCreateOptions {
params: Some(json!({ "authToken": "jwt-token-here" })),
..Default::default()
},
)?;
let connection = chat.connect();
connection.disconnect().await;
Ok(())
}
Subscribing to Events
on registers a typed callback for an event and returns once the subscription is registered:
use counter::{Counter, NewCount};
use rivetkit::{
client::{Client, ClientConfig},
prelude::*,
TypedClientExt,
};
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
let connection = client
.get_or_create_typed_default::<Counter>("counter", ["general"])?
.connect();
connection
.on::<NewCount>(|event| println!("count changed: {}", event.count))
.await;
Ok(())
}
Event callbacks are synchronous and run for every matching event. The actor's emitted event type (here NewCount) is decoded into the typed value for you.
Connection Lifecycle
The lower-level connection exposes lifecycle callbacks and the current status. Reach it with connection.inner():
use counter::Counter;
use rivetkit::{
client::{Client, ClientConfig},
prelude::*,
TypedClientExt,
};
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
let connection = client
.get_or_create_typed_default::<Counter>("counter", ["general"])?
.connect();
let inner = connection.inner().clone();
inner.on_open(|| println!("connected")).await;
inner.on_close(|| println!("disconnected")).await;
inner.on_error(|message| eprintln!("error: {message}")).await;
inner
.on_status_change(|status| println!("status: {status:?}"))
.await;
println!("current status: {:?}", inner.conn_status());
connection.disconnect().await;
Ok(())
}
ConnectionStatus is one of Idle, Connecting, Connected, or Disconnected. Connections reconnect automatically with backoff until you call disconnect.
Low-Level HTTP & WebSocket
For actors that implement on_request or on_websocket, call them directly on the untyped handle (handle.inner()). fetch returns a reqwest::Response, and web_socket returns a tokio_tungstenite stream. This example also needs a few extra crates:
cargo add futures-util tokio-tungstenite
cargo add reqwest --features json
use counter::Counter;
use futures_util::{SinkExt, StreamExt};
use reqwest::{header::HeaderMap, Method};
use rivetkit::{
client::{Client, ClientConfig},
prelude::*,
TypedClientExt,
};
use tokio_tungstenite::tungstenite::Message;
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
let handle = client.get_or_create_typed_default::<Counter>("counter", ["general"])?;
let response = handle
.inner()
.fetch("history", Method::GET, HeaderMap::new(), None)
.await?;
let history: Vec<String> = response.json().await?;
println!("history: {history:?}");
let mut ws = handle.inner().web_socket("stream", None).await?;
ws.send(Message::text("hello")).await?;
if let Some(message) = ws.next().await {
println!("received: {:?}", message?);
}
Ok(())
}
Calling from Backend
The client is a normal Tokio type, so you can hold it in your backend (Axum, Actix, etc.) and call actors from request handlers. The client is Clone and cheap to share:
use counter::{Counter, Increment};
use rivetkit::{
client::{Client, ClientConfig},
prelude::*,
TypedClientExt,
};
async fn increment(client: Client) -> Result<i64> {
let counter = client.get_or_create_typed_default::<Counter>("counter", ["server-counter"])?;
let count = counter.send(Increment { amount: 1 }).await?;
Ok(count)
}
Error Handling
Action and connection calls return anyhow::Result. Actor-side errors surface as an anyhow::Error carrying the error group, code, message, and metadata:
use counter::{Counter, Increment};
use rivetkit::{
client::{Client, ClientConfig},
prelude::*,
TypedClientExt,
};
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
let counter = client.get_or_create_typed_default::<Counter>("counter", ["my-counter"])?;
match counter.send(Increment { amount: 1 }).await {
Ok(count) => println!("count: {count}"),
Err(error) => eprintln!("action failed: {error:#}"),
}
Ok(())
}
Concepts
Keys
Keys uniquely identify actor instances. Use compound keys (arrays) for hierarchical addressing:
use counter::Counter;
use rivetkit::{
client::{Client, ClientConfig},
prelude::*,
TypedClientExt,
};
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::new(ClientConfig::new("http://localhost:6420").namespace("default"));
let room = client.get_or_create_typed_default::<Counter>("counter", ["org-acme", "general"])?;
let actor_id = room.inner().resolve().await?;
println!("Actor ID: {actor_id}");
Ok(())
}
Keys accept arrays of &str or String (["org-acme", "general"]). Don't build keys with string interpolation like format!("org:{user_id}") when user_id contains user data. Use arrays instead to prevent key injection attacks.
Configuration
ClientConfig::new(endpoint) is a builder. The endpoint is always required; there is no default. Common options:
use rivetkit::client::ClientConfig;
let config = ClientConfig::new("http://localhost:6420")
.namespace("default")
.token("pk_...")
.pool_name("my-pool")
.header("x-custom", "value");
namespace - target namespace (defaults to the engine's configured namespace).
token - authentication token for the engine.
pool_name - runner pool to target.
header / headers - extra HTTP headers sent with each request.
max_input_size - cap on encoded action input size.
For serverless deployments, set the endpoint to your app's /api/rivet URL. See Endpoints for details.
API Reference
See the full client API documentation on docs.rs/rivetkit-client.
Need More Than the Client?
If you need more about Rivet Actors, registries, or server-side RivetKit, add the main skill:
npx skills add rivet-dev/skills
Then use the rivetkit skill for backend guidance.