SDK & Apps

calimero-sdk · calimero-sdk-macros · calimero-sys · calimero-wasm-abi

Purpose

Proc-macro SDK for building Calimero applications. Developers define state with #[app::state], implement methods with #[app::logic], and emit events with #[app::event]. The SDK compiles to WASM and exposes methods as JSON-RPC-callable endpoints. All persistent collections use CRDT types for conflict-free replicated state across peers.

Proc Macro Attributes

Each attribute transforms user-written Rust into the glue code needed for WASM execution, storage integration, and ABI generation.

#[app::state] — persistent state struct

Marks the persistent state struct. Generates automatic storage keys, versioning info, Borsh derives, and CRDT merge registration. Each field with a CRDT type gets its own storage subtree.

use calimero_sdk::app; use calimero_sdk::borsh::{BorshSerialize, BorshDeserialize}; use calimero_storage::collections::{UnorderedMap, LwwRegister}; #[app::state(emits = for<'a> Event<'a>)] #[derive(Debug, BorshSerialize, BorshDeserialize)] #[borsh(crate = "calimero_sdk::borsh")] pub struct MyApp { items: UnorderedMap<String, LwwRegister<String>>, count: Counter, }
#[app::logic] — WASM-exported methods

Transforms an impl block's public methods into WASM exports. Each method gets input deserialization, output serialization, and error propagation. Private methods are not exported.

#[app::logic] impl MyApp { pub fn set(&mut self, key: String, value: String) -> app::Result<()> { self.items.insert(key, value.into())?; self.count.increment()?; Ok(()) } pub fn get(&self, key: &str) -> app::Result<Option<String>> { Ok(self.items.get(key)?.map(|v| v.get().clone())) } }
#[app::event] — typed event declarations

Declares event types that can be emitted via app::emit!. Events are Borsh-serialized and delivered to subscribers via SSE/WebSocket.

#[app::event] pub enum Event<'a> { ItemSet { key: &'a str, value: &'a str }, ItemRemoved { key: &'a str }, } // Emit without handler (fire-and-forget): app::emit!(Event::ItemSet { key: &k, value: &v }); // Emit with handler (triggers on receiving nodes): app::emit!((Event::ItemSet { key: &k, value: &v }, "on_item_set"));
#[app::init] — initialization hook

Called on first deployment. Sets up initial state and default values. Runs before any logic methods are available.

#[app::init] pub fn init() -> MyApp { MyApp { items: UnorderedMap::new(), count: Counter::new(), } }
#[app::migrate] — state migration between versions

Reads raw bytes from the previous schema, transforms into the new layout, and returns the new state. Use BorshDeserialize::deserialize (not try_from_slice) to handle trailing storage metadata.

#[derive(BorshDeserialize)] struct OldState { items: UnorderedMap<String, LwwRegister<String>>, } #[app::migrate] pub fn migrate_v1_to_v2() -> MyAppV2 { let old_bytes = read_raw().expect("no state"); let old: OldState = BorshDeserialize::deserialize(&mut &old_bytes[..]).unwrap(); MyAppV2 { items: old.items, count: Counter::new(), // new field } }
#[app::destroy] — cleanup hook

Called on application removal. Can perform resource cleanup, final event emission, or state archival before the context is torn down.

#[app::destroy] pub fn cleanup(&self) { app::emit!(Event::AppRemoved {}); app::log!("Application destroyed, state archived"); }
#[app::private] — internal helper methods

Marks a method as internal to the application. Private methods are not exported as WASM entry points and cannot be called via the admin API or JSON-RPC. They can only be called by other methods within the same impl block. Useful for shared validation, computation, or state access helpers.

#[app::logic] impl MyApp { #[app::private] fn validate_key(&self, key: &str) -> bool { !key.is_empty() && key.len() < 256 } pub fn set(&mut self, key: String, value: String) -> app::Result<()> { if !self.validate_key(&key) { app::bail!("invalid key"); } self.items.insert(key, value.into())?; Ok(()) } }
app::emit! — emit events

Emits events from logic methods. Supports fire-and-forget or handler-triggered patterns.

// Fire-and-forget (delivered to SSE/WS clients): app::emit!(Event::ItemSet { key: "foo", value: "bar" }); // With handler (executed on receiving nodes, not author): app::emit!((Event::ItemSet { key: "foo", value: "bar" }, "on_item_set")); // Handler must be commutative, idempotent, and pure: pub fn on_item_set(&mut self, key: &str, value: &str) { self.counters.increment(); // safe: CRDT op }
app::err! / bail! / log! — error handling and logging

Error and logging macros that integrate with the WASM host function ABI.

use thiserror::Error; #[derive(Debug, Error)] #[error("not found: {0}")] pub struct NotFound(String); pub fn get(&self, key: &str) -> app::Result<String> { app::log!("looking up {key}"); let Some(val) = self.items.get(key)? else { app::bail!(NotFound(key.to_owned())); }; Ok(val.get().clone()) }
State / lifecycle
Logic / export
Events / logging

App Lifecycle

From writing application code to executing methods in a live context.

1

Write App with SDK

Define your state struct with #[app::state], implement methods with #[app::logic], declare events with #[app::event]. Use CRDT collections (Map, Set, LwwRegister) for conflict-free replicated state.

2

Build with cargo-mero

Run cargo mero build to compile to .wasm. The build tool runs proc-macro expansion, generates the ABI manifest, and targets wasm32-unknown-unknown. The output is a single .wasm binary ready for installation.

3

Install Application

Use meroctl app install to upload the .wasm binary to a running node. The node stores it as a blob, computes its BlobId (content hash), and registers it in the application catalog.

4

Create Context

Create a context (group + application binding) via the admin API or meroctl context create. This associates the installed application with a new group, sets up storage, and subscribes to the context's gossipsub topic.

5

Execute Methods via JSON-RPC

External clients call methods on the running context via JSON-RPC 2.0. Each call is deserialized, executed in the WASM runtime, and produces a delta that is broadcast to all peers via gossip. State converges automatically.

Write App SDK macros cargo mero build .wasm output meroctl install blob store Create Context group + app JSON-RPC Execute call methods

CRDT Collections

Conflict-free replicated data types available to application state. All implement the Mergeable trait for automatic convergence across peers.

Map<K, V>

Key-value store with per-key merge semantics. Supports nested maps — inner maps merge recursively. Values must implement Mergeable. Used for most application state.

use calimero_sdk::collections::Map;

struct State {
    users: Map<AccountId, UserProfile>,
    nested: Map<String, Map<String, Value>>,
}

Set<T>

Add-wins set. Concurrent additions and removals resolve in favor of additions. Elements must be Eq + Hash + Borsh. Ideal for membership lists, tags, and unique collections.

use calimero_sdk::collections::Set;

struct State {
    members: Set<AccountId>,
    tags: Set<String>,
}

LwwRegister<T>

Last-write-wins register with logical timestamp. Concurrent writes resolve by highest timestamp. Simple but effective for single-value fields where latest value is always preferred.

use calimero_sdk::collections::LwwRegister;

struct State {
    title: LwwRegister<String>,
    config: LwwRegister<AppConfig>,
}

RGA (Replicated Growable Array)

Sequence CRDT for ordered collections and text. Supports concurrent insert, delete, and move operations. Preserves user intent for collaborative editing scenarios.

// Used internally for text/sequence state
// Supports: insert_at, delete_at, move
// Conflict resolution: position-based

Counter

Distributed G-Counter. Each node increments its own slot; value() returns the sum. Naturally commutative and idempotent.

Vector

Ordered append-only list. Items are pushed to the end. Concurrent pushes from different nodes both get appended.

UnorderedMap

Key-value store with LWW (Last-Write-Wins) semantics per entry. Entries inserted, updated, and removed independently across nodes.

Mergeable Trait

Universal merge interface implemented by all CRDT types. The runtime calls merge() automatically when applying incoming deltas from peers. Custom types can implement this trait for domain-specific merge logic.

pub trait Mergeable {
    fn merge(&mut self, other: Self) -> MergeResult;
    fn is_empty(&self) -> bool;
}
Key-value (Map)
Collection (Set)
Register (LWW)
Sequence (RGA)
Trait (Mergeable)

Sample Apps

Reference applications demonstrating SDK features, CRDT patterns, and migration workflows.

kv-store

Basic key-value store using Map<String, String>. Demonstrates #[app::state], #[app::logic], and simple CRUD operations. The canonical "hello world" for Calimero apps.

apps/kv-store

collaborative-editor

Real-time collaborative text editor. Uses RGA for character-level CRDT operations. Demonstrates event emission for cursor position sharing and multi-user editing.

apps/collaborative-editor

access-control

Role-based access control. Uses Map<AccountId, Set<Role>> for per-user role sets. Demonstrates #[app::private] for internal admin checks.

apps/access-control

blobs

Binary large object storage. Demonstrates the blob API for storing and retrieving arbitrary binary data via content-addressed BlobId handles.

apps/blobs

nested-crdt-test

Nested CRDT stress test. Uses Map<K, Map<K, Set<V>>> to verify recursive merge behavior under concurrent modifications.

apps/nested-crdt-test

team-metrics

Dual-implementation app: one using SDK macros, one with a custom ABI. Demonstrates both approaches side-by-side for comparison.

apps/team-metrics

private_data

Private data patterns with encrypted fields and selective disclosure. Uses LwwRegister for per-field encryption keys and #[app::private] for access gates.

apps/private_data

migrations

5-version migration suite demonstrating #[app::migrate] across schema changes. Each version adds fields, renames keys, or restructures the state tree.

apps/migrations

e2e-kv-store

End-to-end test harness for the kv-store app. Runs multi-node scenarios with concurrent writes, verifies CRDT convergence, and validates sync protocol correctness.

apps/e2e-kv-store

Developer Guide

Handler Safety Requirements

Event handlers may execute in parallel across nodes. All handlers must be:

Commutative

Order-independent. Counter increments are safe; operations that depend on prior state (create then update) are not.

Independent

No shared mutable state. Each handler should use unique keys. Two handlers writing the same map key causes a race condition.

Idempotent

Safe to retry. CRDT operations are naturally idempotent. External API calls (payments, notifications) are not.

Pure

No external side effects. Only modify CRDT state and emit logs. HTTP calls, file I/O, and non-deterministic operations are forbidden.

Common Patterns

Counter

Use Counter (G-Counter) for distributed counting. Each node tracks increments separately; value() returns the sum.

Key-Value

Use UnorderedMap<K, LwwRegister<V>> for key-value storage. Last-write-wins on concurrent updates to the same key.

Events

Use app::emit! for fire-and-forget, or app::emit!((event, "handler_name")) to trigger a handler on receiving nodes.

Available Macros

#[app::state] — Mark application state struct #[app::state(emits = Event)] — With events #[app::logic] — Mark implementation block #[app::init] — Mark constructor #[app::event] — Mark event enum #[app::migrate] — Mark migration function app::emit!(event) — Emit event app::log!("msg") — Logging app::bail!(Error::X) — Early return with error

Environment Functions

use calimero_sdk::env; let executor = env::executor_id(); // [u8; 32] let context = env::context_id(); // [u8; 32] let now = env::time_now(); // u64 nanoseconds env::log("Hello from WASM");