# Build all packages in the workspace
cargo build
# Build a specific package
cargo build -p package-name
# Build with release optimizations
cargo build --release# Run all tests in the workspace
cargo test
# Run tests for a specific package
cargo test -p package-name
# Run a specific test
cargo test test_name
# Run tests with output displayed
cargo test -- --nocapture# Format code (enforced by pre-commit hooks)
# cargo fmt
# DO NOT RUN CARGO FMT AUTOMATICALLY (note for humans: we need to run cargo fmt when everything is merged together and make sure lefthook is working)
# Run linter and fix issues
./scripts/cargo/fix.sh
# Check for linting issues
cargo clippy -- -W warnings# Start the development environment with all services
cd docker/dev
docker-compose up -d# When committing changes, use Graphite CLI with conventional commits
gt c -m "chore(my-pkg): foo bar"Never push to main unless explicitly specified by the user.
# Modify a Graphite PR
gt m- Use pnpm for all npm-related commands. We're using a pnpm workspace.
- If you need to look at the documentation for a package, visit
https://docs.rs/{package-name}. For example, serde docs live at https://docs.rs/serde/ - When adding new docs pages, update
website/src/sitemap/mod.tsso the page appears in the sidebar.
Required frontmatter fields:
title(string)description(string)skill(boolean)
Required frontmatter fields:
title(string)description(string)author(enum:nathan-flurry,nicholas-kissel,forest-anderson)published(date string)category(enum:changelog,monthly-update,launch-week,technical,guide,frogs)
Optional frontmatter fields:
keywords(string array)
All example READMEs in /examples/ should follow the format defined in .claude/resources/EXAMPLE_TEMPLATE.md.
This is a Rust workspace-based monorepo for Rivet. Key packages and components:
- Core Engine (
packages/core/engine/) - Main orchestration service that coordinates all operations - Workflow Engine (
packages/common/gasoline/) - Handles complex multi-step operations with reliability and observability - Pegboard (
packages/core/pegboard/) - Actor/server lifecycle management system - Common Packages (
/packages/common/) - Foundation utilities, database connections, caching, metrics, logging, health checks, workflow engine core - Core Packages (
/packages/core/) - Main engine executable, Pegboard actor orchestration, workflow workers - Shared Libraries (
shared/{language}/{package}/) - Libraries shared between the engine and rivetkit (e.g.,shared/typescript/virtual-websocket/) - Service Infrastructure - Distributed services communicate via NATS messaging with service discovery
Error Handling
- Custom error system at
packages/common/error/ - Uses derive macros with struct-based error definitions
To use custom errors:
use rivet_error::*;
use serde::{Serialize, Deserialize};
// Simple error without metadata
#[derive(RivetError)]
#[error("auth", "invalid_token", "The provided authentication token is invalid")]
struct AuthInvalidToken;
// Error with metadata
#[derive(RivetError, Serialize, Deserialize)]
#[error(
"api",
"rate_limited",
"Rate limit exceeded",
"Rate limit exceeded. Limit: {limit}, resets at: {reset_at}"
)]
struct ApiRateLimited {
limit: u32,
reset_at: i64,
}
// Use errors in code
let error = AuthInvalidToken.build();
let error_with_meta = ApiRateLimited { limit: 100, reset_at: 1234567890 }.build();Key points:
- Use
#[derive(RivetError)]on struct definitions - Use
#[error(group, code, description)]or#[error(group, code, description, formatted_message)]attribute - Group errors by module/domain (e.g., "auth", "actor", "namespace")
- Add
Serialize, Deserializederives for errors with metadata fields - Always return anyhow errors from failable functions
- For example:
fn foo() -> Result<i64> { /* ... */ }
- For example:
- Do not glob import (
::*) from anyhow. Instead, import individual types and traits
Dependency Management
- When adding a dependency, check for a workspace dependency in Cargo.toml
- If available, use the workspace dependency (e.g.,
anyhow.workspace = true) - If you need to add a dependency and can't find it in the Cargo.toml of the workspace, add it to the workspace dependencies in Cargo.toml (
[workspace.dependencies]) and then add it to the package you need with{dependency}.workspace = true
Database Usage
- UniversalDB for distributed state storage
- ClickHouse for analytics and time-series data
- Connection pooling through
packages/common/pools/
- Hard tabs for Rust formatting (see
rustfmt.toml) - Follow existing patterns in neighboring files
- Always check existing imports and dependencies before adding new ones
- Always add imports at the top of the file inside of inline within the function.
Data structures often include:
id(uuid)name(machine-readable name, must be valid DNS subdomain, convention is using kebab case)description(human-readable, if applicable)
- Use UUID (v4) for generating unique identifiers
- Store dates as i64 epoch timestamps in milliseconds for precise time tracking
- When storing timestamps, name them *_at with past tense verb. For example, created_at, destroyed_at.
- Use tracing for logging. Do not format parameters into the main message, instead use tracing's structured logging.
- For example, instead of
tracing::info!("foo {x}"), dotracing::info!(?x, "foo")
- For example, instead of
- Log messages should be lowercase unless mentioning specific code symbols. For example,
tracing::info!("inserted UserRow")instead oftracing::info!("Inserted UserRow")
- Do not make changes to docker/dev* configs. Instead, edit the template in docker/template/ and rerun (cd docker/template && pnpm start). This will regenerate the docker compose config for you.
- Do not run ./scripts/cargo/fix.sh. Do not format the code yourself.
- When running tests, always pipe the test to a file in /tmp/ then grep it in a second step. You can grep test logs multiple times to search for different log lines.
- For RivetKit TypeScript tests, run from
rivetkit-typescript/packages/rivetkitand usepnpm test <filter>with-tto narrow to specific suites. For example:pnpm test driver-file-system -t ".*Actor KV.*".
- Never build a new reqwest client from scratch. Use
rivet_pools::reqwest::client().await?to access an existing reqwest client instance.
- When talking about "Rivet Actors" make sure to capitalize "Rivet Actor" as a proper noun and lowercase "actor" as a generic noun
- Write comments as normal, complete sentences. Avoid fragmented structures with parentheticals and dashes like
// Spawn engine (if configured) - regardless of start kind. Instead, write// Spawn the engine if configured. Especially avoid dashes (hyphens are OK). - Documenting deltas is not important or useful. A developer who has never worked on the project will not gain extra information if you add a comment stating that something was removed or changed because they don't know what was there before. The only time you would be adding a comment for something NOT being there is if its unintuitive for why its not there in the first place.
- When adding new examples, or updating existing ones, ensure that the user also modified the vercel equivalent, if applicable. This ensures parity between local and vercel examples. In order to generate vercel example, run
./scripts/vercel-examples/generate-vercel-examples.tsafter making changes to examples. - To skip Vercel generation for a specific example, add
"skipVercel": trueto thetemplateobject in the example'spackage.json.
After regenerating Vercel examples, you may see type check errors like:
error TS2688: Cannot find type definition file for 'vite/client'.
with warnings about node_modules missing. This happens because the regenerated examples need their dependencies reinstalled. Fix by running pnpm install before running type checks.