Rust Quickstart (Beta)
Build a Rivet Actor in Rust
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.
Steps
Add Rivet
Add the rivetkit crate and its companions:
cargo add rivetkit anyhow async-trait
cargo add serde --features derive
cargo add tokio --features full
Define Your Actor
Put the actor in src/lib.rs so both your server and your client can share the same types. An actor is a type that implements Actor, plus one Handles<M> implementation for each action. Persisted state lives in type State; ephemeral runtime state is just fields on your actor struct.
use std::{future::Future, pin::Pin, sync::Arc};
use async_trait::async_trait;
use rivetkit::prelude::*;
use serde::{Deserialize, Serialize};
type BoxFuture<T> = Pin<Box<dyn Future<Output = Result<T>> + Send>>;
pub struct Counter;
#[derive(Default, Serialize, Deserialize)]
pub struct CounterState {
pub count: i64,
}
#[derive(Serialize, Deserialize)]
pub struct Increment {
pub amount: i64,
}
impl Action for Increment {
type Output = i64;
const NAME: &'static str = "increment";
}
#[derive(Serialize, Deserialize)]
pub struct NewCount {
pub count: i64,
}
impl Event for NewCount {
const NAME: &'static str = "newCount";
}
#[async_trait]
impl Actor for Counter {
type State = CounterState;
type Input = ();
type Actions = (Increment,);
type Events = (NewCount,);
type Queue = ();
type ConnParams = ();
type ConnState = ();
type Action = action::Raw;
async fn create_state(_ctx: &Ctx<Self>, _input: Self::Input) -> Result<Self::State> {
Ok(CounterState::default())
}
async fn create(_ctx: &Ctx<Self>) -> Result<Self> {
Ok(Self)
}
}
impl Handles<Increment> for Counter {
type Future = BoxFuture<i64>;
fn handle(self: Arc<Self>, ctx: Ctx<Self>, action: Increment) -> Self::Future {
Box::pin(async move {
let count = {
let mut state = ctx.state_mut();
state.count += action.amount;
state.count
};
ctx.emit(NewCount { count })?;
Ok(count)
})
}
}
pub fn registry() -> Registry {
let mut registry = Registry::new();
registry.register_actor::<Counter>("counter");
registry
}
Serve The Registry
Your src/main.rs just starts the registry from the library:
#[tokio::main]
async fn main() -> anyhow::Result<()> {
counter::registry().start().await
}
Replace counter with your crate name (the package name in Cargo.toml, with dashes as underscores).
Run The Server
The Rust runtime connects to the Rivet control plane. Setting RIVETKIT_ENGINE_AUTO_DOWNLOAD=1 lets the runtime download and cache a matching engine binary the first time you run, so there is nothing else to install:
RIVETKIT_ENGINE_AUTO_DOWNLOAD=1 cargo run
Your server now connects to the Rivet control plane on http://localhost:6420. Clients connect directly to the control plane on this port.
Visit http://localhost:6420 in your browser (or point your AI agent at it) to open the Rivet developer tools and inspect your actors live.
Already have a control plane binary? Set RIVET_ENGINE_BINARY_PATH=/path/to/rivet-engine to point at it instead. If you are working inside the Rivet monorepo, a local cargo build -p rivet-engine is discovered automatically from target/debug.
Connect To The Rivet Actor
This code can run either in your frontend or within your backend:
Add a src/bin/client.rs that imports the same actor types from your library. There is no need to redefine the actor on the client.
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"])?;
let count = counter.send(Increment { amount: 3 }).await?;
println!("New count: {count}");
let connection = counter.connect();
connection
.on::<NewCount>(|event| println!("Count changed: {}", event.count))
.await;
connection.send(Increment { amount: 1 }).await?;
Ok(())
}
With the server still running, start the client in another terminal:
cargo run --bin client
See the hello-world-rust example for a complete runnable counter.
A TypeScript client can call your Rust actor by name through the same engine. Actor and action names are resolved at runtime, so the client is untyped here:
import { createClient } from "rivetkit/client";
const client = createClient("http://localhost:6420");
const counter = client.counter.getOrCreate(["my-counter"]);
const counterConnection = counter.connect();
counterConnection.on("newCount", (event) => {
console.log("Event count:", event.count);
});
const count = await counterConnection.increment(3);
console.log("New count:", count);
await counterConnection.increment(1);
See the JavaScript client documentation for more information.
import { createRivetKit } from "@rivetkit/react";
import { useState } from "react";
const { useActor } = createRivetKit("http://localhost:6420");
function Counter() {
const [count, setCount] = useState(0);
const counter = useActor({
name: "counter",
key: ["my-counter"],
});
const increment = async () => {
await counter.connection?.increment(1);
};
counter.useEvent("newCount", (event) => {
setCount(event.count);
});
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
See the React documentation for more information.
Deploy
By default, Rivet stores actor state on the local file system.
To scale Rivet in production, pick how much of it you want to run yourself:
Fully managed
Bring your own compute
Full self-hosting
If you are running your own workers, follow the guide for your hosting provider: