mcpField
R
rivetkit-client-rust

rivetkit-client-rust

github.com/rivet-devDevelopment44888 installsUpdated 1d ago
Repository

About rivetkit-client-rust

A skill for building Rust clients that connect to Rivet Actors with `rivetkit::client`. It covers typed actions and events, connection handling, configuration, and backend usage.

Key features

  1. 01Strongly typed actor actions and events
  2. 02Stateless calls and persistent connections
  3. 03Typed event subscriptions
  4. 04Automatic reconnection with backoff
  5. 05Low-level HTTP and WebSocket calls

Use cases

  1. 01Calling Rivet Actors from a Rust application
  2. 02Receiving typed actor events over a connection
  3. 03Calling actors from Tokio backend handlers

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/rivet-dev/skills --skill rivetkit-client-rust

After installing, type/rivetkit-client-rustin Claude Code to confirm it loaded.

Bundled filesindex.json

Details

From the project README

RivetKit Rust Client

Use this skill when building Rust clients that connect to Rivet Actors with rivetkit::client.

Version

RivetKit version: 2.3.7

First Steps

  1. Add the dependency
    cargo add rivetkit anyhow async-trait
    cargo add serde --features derive
    cargo add tokio --features full
    
  2. 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"])?;

	// Stateless: each call is independent
	counter.send(Increment { amount: 1 }).await?;

	// Stateful: keep a connection open for realtime events
	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"));

	// Get or create an actor
	let room = client.get_or_create_typed_default::<Counter>("counter", ["room-42"])?;

	// Get an existing actor handle (fails when used if the actor does not exist)
	let existing = client.get_typed_default::<Counter>("counter", ["room-42"])?;

	// Create a new actor with input
	let created = client.get_or_create_typed::<Counter>(
		"counter",
		["game-1"],
		GetOrCreateOptions {
			create_with_input: Some(json!({ "mode": "ranked" })),
			..Default::default()
		},
	)?;

	// Get an actor handle by ID
	let by_id = client.get_for_id("counter", "actor-id", Default::default())?;

	// Resolve the actor ID
	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. Set pool_name on GetOrCreateOptions or CreateOptions to override the client's configured pool for a single actor.

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"])?;

	// Raw HTTP request
	let response = handle
		.inner()
		.fetch("history", Method::GET, HeaderMap::new(), None)
		.await?;
	let history: Vec<String> = response.json().await?;
	println!("history: {history:?}");

	// Raw WebSocket connection
	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"));

	// Compound key: [org, room]
	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.

Comments

Markdown supported