Skip to main content
Quickstart

Rust Quickstart

Build and run a Rivet Actor in Rust, store data in SQLite and define actions, then connect a client to the actor during local development.

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 Handler<M> implementation for each action. Durable data lives in the actor’s SQLite database (ctx.sql()); in-memory data is just fields on your actor struct.

use std::sync::Arc;

use async_trait::async_trait;
use rivetkit::prelude::*;
use rivetkit::{BindParam, ColumnValue};
use serde::{Deserialize, Serialize};

pub struct Counter;

#[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 Input = ();
	type Actions = (Increment,);
	type Events = (NewCount,);
	type ConnParams = ();
	type Action = action::Raw;

	async fn create(ctx: &Ctx<Self>) -> Result<Self> {
		ctx.sql()
			.exec(
				"CREATE TABLE IF NOT EXISTS counter (
					id INTEGER PRIMARY KEY,
					count INTEGER NOT NULL
				);
				INSERT OR IGNORE INTO counter (id, count) VALUES (0, 0);",
			)
			.await?;
		Ok(Self)
	}
}

impl Handler<Increment> for Counter {
	async fn handle(self: Arc<Self>, ctx: Ctx<Self>, action: Increment) -> Result<i64> {
		let result = ctx
			.sql()
			.execute(
				"UPDATE counter SET count = count + ? WHERE id = 0 RETURNING count",
				Some(vec![BindParam::Integer(action.amount)]),
			)
			.await?;
		let count = match result.rows.first().and_then(|row| row.first()) {
			Some(ColumnValue::Integer(count)) => *count,
			other => return Err(anyhow!("expected integer count, got {other:?}")),
		};
		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.

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:

Rust

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

	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.

TypeScript

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.

React

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

In development, Rivet runs a local control plane for you and keeps Actor data on your machine.

To scale Rivet in production, pick how much of it you want to run yourself:

If you are running your own nodes, follow the guide for your hosting provider:

Next Steps

Edit this page Last updated November 10, 2026