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, Connection};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to the server
let es = EvidentSource::connect("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};
#[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("http://localhost:50051").await?;
// Create or connect to a database
let db_name = "banking".to_string();
es.create_database(db_name.clone()).await.ok(); // Ignore if exists
let conn = es.connect_database(db_name).await?;
println!("Connected to database at revision {}", conn.revision());
// Execute a state change
let db = conn.execute_state_change(
"open-account",
1,
serde_json::json!({
"accountId": "acct-001",
"customerName": "Alice Smith",
"initialDeposit": 1000.00
}),
).await?;
println!("New revision: {}", db.revision());
// Query a state view
let account = db.view_state::<AccountSummary>(
"account-summary",
1,
Some(&[("account_id", "acct-001")]),
).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};
use uuid::Uuid;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct OpenAccountCommand {
account_id: String,
customer_name: String,
initial_deposit: f64,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct AccountOpened {
account_id: String,
customer_name: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct AccountCredited {
amount: f64,
description: String,
}
#[state_change]
fn open_account(
db: &impl Database,
cmd: OpenAccountCommand,
_meta: StateChangeMetadata,
) -> StateChangeResult {
// Validate initial deposit
if cmd.initial_deposit < 0.0 {
return Err(StateChangeError::validation(
"Initial deposit cannot be negative"
));
}
// Check if account already exists
let existing = db.view_state::<serde_json::Value>(
"account-summary",
1,
&[("account_id", &cmd.account_id)],
)?;
if existing.is_some() {
return Err(StateChangeError::conflict("Account already exists"));
}
let mut result = DecideResult::new();
// Emit AccountOpened event
let opened_event = CloudEventBuilder::new()
.id(Uuid::new_v4().to_string())
.event_type("com.banking.account.opened")
.subject(&cmd.account_id)
.stream("accounts")
.json_data(&AccountOpened {
account_id: cmd.account_id.clone(),
customer_name: cmd.customer_name,
})?
.build();
result = result.event(opened_event);
// Emit initial deposit if non-zero
if cmd.initial_deposit > 0.0 {
let credit_event = CloudEventBuilder::new()
.id(Uuid::new_v4().to_string())
.event_type("com.banking.account.credited")
.subject(&cmd.account_id)
.stream("accounts")
.json_data(&AccountCredited {
amount: cmd.initial_deposit,
description: "Initial deposit".to_string(),
})?
.build();
result = result.event(credit_event);
}
// DCB constraint: ensure account doesn't already exist
result = result.condition(fail_if_events_match(
Subject::equals(&cmd.account_id)
.and(EventType::equals("com.banking.account.opened"))
));
Ok(result)
}

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};
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AccountSummary {
id: String,
name: String,
balance: f64,
is_open: bool,
}
#[state_view]
fn account_summary(state: Option<AccountSummary>, events: Vec<Event>) -> Option<AccountSummary> {
let mut summary = state.unwrap_or_default();
for event in events {
match event.event_type() {
"com.banking.account.opened" => {
if let Ok(data) = event.parse_data::<AccountOpenedData>() {
summary.id = data.account_id;
summary.name = data.customer_name;
summary.is_open = true;
}
}
"com.banking.account.credited" => {
if let Ok(data) = event.parse_data::<AccountCreditedData>() {
summary.balance += data.amount;
}
}
"com.banking.account.debited" => {
if let Ok(data) = event.parse_data::<AccountDebitedData>() {
summary.balance -= data.amount;
}
}
"com.banking.account.closed" => {
summary.is_open = false;
}
_ => {}
}
}
Some(summary)
}
#[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,
}

Deploy your WASM components to the EvidentSource server:

Terminal window
# Create database
curl -X POST http://localhost:8080/api/v1/databases/banking
# Install state change
curl -X POST http://localhost:8080/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:8080/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());
}
}