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.
#[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::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::init] — initialization hook
Called on first deployment. Sets up initial state and default values. Runs before any logic methods are available.
#[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.
#[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::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::emit! — emit events
Emits events from logic methods. Supports fire-and-forget or handler-triggered patterns.
app::err! / bail! / log! — error handling and logging
Error and logging macros that integrate with the WASM host function ABI.
App Lifecycle
From writing application code to executing methods in a live context.
Write App with SDK
Define your state struct with #[app::state], implement methods with #[app::logic], declare events with #[app::event]. Use CRDT collections (UnorderedMap, UnorderedSet, LwwRegister) for conflict-free replicated state.
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.
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.
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.
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.
CRDT Collections
Conflict-free replicated data types available to application state. All implement the Mergeable trait for automatic convergence across peers.
UnorderedMap<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.
struct State {
users: UnorderedMap<AccountId, UserProfile>,
nested: UnorderedMap<String, UnorderedMap<String, Value>>,
}
UnorderedSet<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.
struct State {
members: UnorderedSet<AccountId>,
tags: UnorderedSet<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.
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.
// Supports: insert_at, delete_at, move
// Conflict resolution: position-based
Counter
Distributed counter generic over ALLOW_DECREMENT: bool. The default (Counter<false>, aliased GCounter) is grow-only — each node increments its own slot and value() returns the sum. Set the flag to true (alias PNCounter) to also support decrement, at the cost of a second per-node map. 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.
Storage Primitives
Beyond the public CRDT collections above, the storage layer offers wrappers that constrain who can write what. Most are signature-based: after your method returns, the context manager signs the executor's actions with their identity key, and peers verify those signatures at merge time using the runtime's ed25519_verify host function. WASM code itself does not produce signatures — it only stamps placeholders that get filled in post-execution. FrozenStorage is the exception: it has no per-identity check, immutability is structural (enforced by content-addressing). You don't need to call any signing API; pick the primitive whose guarantee matches your data and the trust model follows from the type. See crates/store for the full enforcement details.
See crates/store for the full comparison table including storage stamps, mutation rules, and when composition does (and does not) substitute for these primitives.
UserStorage<T>
Per-user slot keyed by PublicKey. The executor can only write into their own slot; reads are unrestricted. Use for per-member preferences, profiles, or private-but-replicated state.
struct State {
profile: UserStorage<Profile>,
}
SharedStorage<T>
A single value writable by any signer in a mutable writer set. Any current writer can rotate the set unless it is frozen. Use for group-managed config or shared documents with a controlled author list.
struct State {
policy: SharedStorage<Policy>,
}
FrozenStorage<T>
Content-addressable immutable storage. insert returns a SHA-256 hash; reads are by hash. Same value always yields the same hash; entries cannot be updated. Use for attachments, snapshots, or any data that should be append-only and de-duplicated. Note: unlike the others in this section, FrozenStorage's authorization is structural — anyone can insert, immutability is enforced by content-addressing, no per-identity signature is checked at merge time.
struct State {
attachments: FrozenStorage<Vec<u8>>,
}
AuthoredMap / AuthoredVector
Shared keyspace with per-entry ownership. Any member can insert/push, but only the original author can update, remove (Map), or tombstone (Vector) their own entries. Use for posts, comments, or any append-friendly collection where authorship matters.
struct State {
posts: AuthoredMap<PostId, Post>,
feed: AuthoredVector<Event>,
}
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.
fn merge(&mut self, other: Self) -> MergeResult;
fn is_empty(&self) -> bool;
}
Mergeable State Lint
A compile-time check, applied by #[app::state] and #[derive(Mergeable)], that rejects field types which cannot participate in CRDT merging. Without it, those fields silently fail to converge across replicas — the worst class of distributed-systems bug, because there is no error and no log line, just data that quietly disagrees.
The problem
In Calimero, two nodes can independently update the same logical state and later sync. Convergence requires every persistent field to know how to merge with another copy of itself — that's the contract Mergeable encodes. Std collections (HashMap, BTreeMap, Vec, ...) and bare primitives (String, u64, ...) have no such contract; before this lint, they would compile fine, get persisted as opaque blobs, and silently diverge.
A particularly subtle variant: LwwRegister<HashMap<K, V>>. The wrapper makes the whole field Mergeable, so the type system accepts it — but merging happens at the register level (last-write-wins on the entire map), not at the map entry level. Two nodes inserting different keys concurrently will see one set of inserts win and the other vanish.
What the lint rejects
Walking the AST of every field in a #[app::state] struct (and every field of every #[derive(Mergeable)] struct), recursing into generic arguments:
- Forbidden anywhere in the type tree: HashMap, BTreeMap, HashSet, BTreeSet, LinkedList, VecDeque. These have no merge semantics at any depth — including inside LwwRegister<_>.
- Forbidden at the field root only: bare Vec, bare String, bare primitives (u8..u128, i8..i128, f32/f64, bool, char). Inside CRDT collections their use is fine — e.g. UnorderedMap<String, _> uses String as a key, which is just bytes; only the value side has to be Mergeable.
- Allowed transparently: Option<T> and Box<T> pass through to their inner type; the lint applies as if the wrapper weren't there.
Example diagnostic
#[derive(Mergeable)]
Companion proc-macro for user-defined structs that need to live inside CRDT collections (UnorderedMap<_, V>, Vector<V>, ...). Runs the same field-level lint and generates a field-by-field Mergeable impl that delegates to each field's own merge(). Re-exported as calimero_sdk::app::Mergeable.
use calimero_storage::collections::{Counter, LwwRegister};
#[derive(Mergeable)]
pub struct UserStats {
visits: Counter,
name: LwwRegister<String>,
}
Enums are rejected by the derive — there is no canonical merge rule for "left side is VariantA, right side is VariantB". Wrap in LwwRegister<MyEnum> for last-write-wins, or implement Mergeable by hand.
Escape hatch
There is no attribute opt-out. The only way to put a non-CRDT field in persistent state is to not use #[app::state] / #[derive(Mergeable)] and implement Mergeable by hand. This is intentional: any non-CRDT field will not converge, and we want that decision to require explicit code, not a flag. The hand-rolled impl on FileRecord in apps/blobs is a worked example — atomic LWW on an uploaded_at timestamp, justified inline.
Sample Apps
Reference applications demonstrating SDK features, CRDT patterns, and migration workflows.
kv-store
Basic key-value store using UnorderedMap<String, LwwRegister<String>>. Demonstrates #[app::state], #[app::logic], and simple CRUD operations. The canonical "hello world" for Calimero apps.
apps/kv-storecollaborative-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-editoraccess-control
Role-based access control. Uses UnorderedMap<AccountId, UnorderedSet<Role>> for per-user role sets. Demonstrates #[app::private] for internal admin checks.
apps/access-controlblobs
Binary large object storage. Demonstrates the blob API for storing and retrieving arbitrary binary data via content-addressed BlobId handles.
apps/blobsnested-crdt-test
Nested CRDT stress test. Uses UnorderedMap<K, UnorderedMap<K, UnorderedSet<V>>> to verify recursive merge behavior under concurrent modifications.
apps/nested-crdt-testteam-metrics
Dual-implementation app: one using SDK macros, one with a custom ABI. Demonstrates both approaches side-by-side for comparison.
apps/team-metricsprivate_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_datamigrations
5-version migration suite demonstrating #[app::migrate] across schema changes. Each version adds fields, renames keys, or restructures the state tree.
apps/migrationse2e-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-storeDeveloper 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.