Skip to content

Getting Started with Rust

This guide walks you through building a complete EvidentSource application using the Rust SDK.

  • Rust 1.84 or later
  • For WASM components: rustup target add wasm32-wasip2
  • An EvidentSource server instance (local or cloud)

The fastest way to get started is using the project template with cargo-generate:

Terminal window
# Install cargo-generate if needed
cargo install cargo-generate
# Create a new project from template
cargo generate --git https://github.com/evidentsystems/evidentsource-sdks \
rust/functions/templates/evidentsource-state-management \
--name my-app

The template prompts for:

  • project_name: Your project name (e.g., my-app)
  • database_name: Target database name (e.g., my-database)
  • first_state_change: Initial state change name (e.g., open-account)
  • first_state_view: Initial state view name (e.g., account-summary)

This creates a complete project with:

  • domain/ - Shared types for events and commands
  • state-changes/ - Example state change component
  • state-views/ - Example state view component
  • build_all.sh - Build all WASM components
  • install.sh - Deploy components to EvidentSource
  • CLAUDE.md - AI assistant context for development
Terminal window
cd my-app
./build_all.sh # Build WASM components
./install.sh --all # Deploy to server

See the Template Guide for full documentation.

If you prefer to set up manually or need a client-only application:

Terminal window
cargo new my-banking-app
cd my-banking-app

Update Cargo.toml:

[package]
name = "my-banking-app"
version = "0.1.0"
edition = "2021"
[dependencies]
evidentsource-client = { git = "https://github.com/evidentsystems/evidentsource-sdks", branch = "main" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "1.0", features = ["v4"] }
[lib]
crate-type = ["cdylib"]
[profile.release]
opt-level = "s"
lto = true

Create a client and connect to the server:

use evidentsource_client::EvidentSource;
use evidentsource_core::domain::DatabaseName;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to the server
let es = EvidentSource::connect_to_server("http://localhost:50051").await?;

For production deployments, you’ll need to authenticate with a JWT token. See Authentication for details on the trust model.

use evidentsource_client::{EvidentSource, Credentials};
use evidentsource_client::client::DatabaseAtRevisionTyped;
use evidentsource_core::domain::{DatabaseName, StateViewName, EventAttribute};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Bearer token authentication (production)
let token = std::env::var("EVS_TOKEN")?;
let es = EvidentSource::connect_with_auth(
"https://api.example.com:50051",
Credentials::BearerToken(token),
).await?;
// DevMode for local development (no auth required)
let es = EvidentSource::connect_to_server("http://localhost:50051").await?;
// Create or connect to a database
let db_name = DatabaseName::new("banking")?;
es.create_database(db_name.clone()).await.ok(); // Ignore if exists
let conn = es.connect(&db_name).await?;
println!("Connected to database at revision {}", conn.local_database().revision());
// Execute a state change using builder pattern
let db = conn
.state_change("open-account", 1)
.json(&serde_json::json!({
"accountId": "acct-001",
"customerName": "Alice Smith",
"initialDeposit": 1000.00
}))?
.execute()
.await?;
println!("New revision: {}", db.revision());
// Query a state view
let sv_name = StateViewName::new("account-summary")?;
let account: Option<AccountSummary> = db.view_state_opt(&sv_name, 1).await?;
if let Some(acc) = account {
println!("Balance: ${:.2}", acc.balance);
}
Ok(())
}
#[derive(Debug, serde::Deserialize)]
struct AccountSummary {
id: String,
name: String,
balance: f64,
}

State changes handle commands and emit events. Create a new crate for your state change:

Terminal window
cargo new --lib state-changes/open-account

Update state-changes/open-account/Cargo.toml:

[package]
name = "open-account"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
evidentsource-functions = { git = "https://github.com/evidentsystems/evidentsource-sdks", branch = "main" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "1.0", features = ["v4"] }

Implement the state change in src/lib.rs:

use evidentsource_functions::prelude::*;
use serde::{Deserialize, Serialize};
// Command type - parsed from JSON request body
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenAccountCommand {
pub account_id: String,
pub customer_name: String,
pub initial_deposit: f64,
}
// Implement TryFrom<Command> to parse the command
impl TryFrom<Command> for OpenAccountCommand {
type Error = String;
fn try_from(cmd: Command) -> Result<Self, Self::Error> {
serde_json::from_slice(cmd.body())
.map_err(|e| format!("Failed to parse command: {}", e))
}
}
// Domain events - implement Into<ProspectiveEvent> for each
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountOpened {
pub account_id: String,
pub customer_name: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountCredited {
pub account_id: String,
pub amount: f64,
pub description: String,
}
// The state change function - annotated with the macro
#[evidentsource_functions::state_change(name = "open-account")]
fn decide(
db: &Database,
cmd: OpenAccountCommand,
_metadata: &StateChangeMetadata,
) -> Result<(NonEmpty<impl Into<ProspectiveEvent>>, Vec<AppendCondition>), StateChangeError> {
// Validate initial deposit
if cmd.initial_deposit <= 0.0 {
return Err(StateChangeError::Validation(
"Initial deposit must be greater than zero".into(),
));
}
// Check if account already exists via state view
let existing = db.view_state("account-summary", 1, &[("account_id", &cmd.account_id)])?;
if existing.is_some() {
return Err(StateChangeError::Conflict("Account already exists".into()));
}
// Build domain events
let events = vec![
AccountOpened {
account_id: cmd.account_id.clone(),
customer_name: cmd.customer_name,
}.into_prospective_event("com.banking.account.opened", &cmd.account_id),
AccountCredited {
account_id: cmd.account_id.clone(),
amount: cmd.initial_deposit,
description: "Initial deposit".into(),
}.into_prospective_event("com.banking.account.credited", &cmd.account_id),
];
// DCB constraint: account must not already exist
let constraints = vec![
AppendCondition::max(
EventSelector::subject_equals(&cmd.account_id)
.map_err(|e| StateChangeError::Internal(e.to_string()))?
.and(
EventSelector::event_type_equals("com.banking.account.opened")
.map_err(|e| StateChangeError::Internal(e.to_string()))?,
),
0, // Max 0 matching events = must not exist
),
];
Ok((NonEmpty::from_vec(events).expect("Events produced"), constraints))
}

Build the WASM component:

Terminal window
cd state-changes/open-account
cargo build --release --target wasm32-wasip2

State views materialize read models from events. Create a new crate:

Terminal window
cargo new --lib state-views/account-summary

Implement the state view:

use evidentsource_functions::prelude::*;
use serde::{Deserialize, Serialize};
// State type - serialized to/from JSON
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountSummary {
pub id: String,
pub name: String,
pub balance: f64,
pub is_open: bool,
}
// Event data types for parsing
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct AccountOpenedData {
account_id: String,
customer_name: String,
}
#[derive(Deserialize)]
struct AccountCreditedData {
amount: f64,
}
#[derive(Deserialize)]
struct AccountDebitedData {
amount: f64,
}
// Convert Event to typed domain event
enum AccountEvent {
Opened(AccountOpenedData),
Credited(AccountCreditedData),
Debited(AccountDebitedData),
Closed,
Unknown,
}
impl TryFrom<&Event> for AccountEvent {
type Error = String;
fn try_from(event: &Event) -> Result<Self, Self::Error> {
match event.event_type() {
"com.banking.account.opened" => {
let data: AccountOpenedData = serde_json::from_slice(
event.data().ok_or("Missing data")?.as_bytes()
).map_err(|e| e.to_string())?;
Ok(AccountEvent::Opened(data))
}
"com.banking.account.credited" => {
let data: AccountCreditedData = serde_json::from_slice(
event.data().ok_or("Missing data")?.as_bytes()
).map_err(|e| e.to_string())?;
Ok(AccountEvent::Credited(data))
}
"com.banking.account.debited" => {
let data: AccountDebitedData = serde_json::from_slice(
event.data().ok_or("Missing data")?.as_bytes()
).map_err(|e| e.to_string())?;
Ok(AccountEvent::Debited(data))
}
"com.banking.account.closed" => Ok(AccountEvent::Closed),
_ => Ok(AccountEvent::Unknown),
}
}
}
// The state view function - processes one event at a time
#[evidentsource_functions::state_view(name = "account-summary")]
fn evolve(mut state: AccountSummary, event: AccountEvent) -> AccountSummary {
match event {
AccountEvent::Opened(data) => {
state.id = data.account_id;
state.name = data.customer_name;
state.is_open = true;
}
AccountEvent::Credited(data) => {
state.balance += data.amount;
}
AccountEvent::Debited(data) => {
state.balance -= data.amount;
}
AccountEvent::Closed => {
state.is_open = false;
}
AccountEvent::Unknown => {}
}
state
}

Deploy your WASM components to the EvidentSource server:

Terminal window
# Create database
curl -X POST http://localhost:3000/api/v1/databases/banking
# Install state change
curl -X POST http://localhost:3000/api/v1/databases/banking/state-changes/open-account/versions/1 \
--data-binary @target/wasm32-wasip2/release/open_account.wasm \
-H "Content-Type: application/wasm"
# Install state view
curl -X POST http://localhost:3000/api/v1/databases/banking/state-views/account-summary/versions/1 \
--data-binary @target/wasm32-wasip2/release/account_summary.wasm \
-H "Content-Type: application/wasm"

Use the mock database for unit testing:

#[cfg(test)]
mod tests {
use super::*;
use evidentsource_functions::testing::MockDatabase;
#[test]
fn test_open_account_success() {
let db = MockDatabase::new("test-db").with_revision(100);
let cmd = OpenAccountCommand {
account_id: "acct-001".to_string(),
customer_name: "Alice".to_string(),
initial_deposit: 100.0,
};
let result = open_account(&db, cmd, StateChangeMetadata::default());
assert!(result.is_ok());
let decide = result.unwrap();
assert_eq!(decide.events.len(), 2); // opened + credited
}
#[test]
fn test_open_account_negative_deposit() {
let db = MockDatabase::new("test-db");
let cmd = OpenAccountCommand {
account_id: "acct-001".to_string(),
customer_name: "Alice".to_string(),
initial_deposit: -50.0,
};
let result = open_account(&db, cmd, StateChangeMetadata::default());
assert!(result.is_err());
}
}