Skip to content

Example Apps

The apps/ directory is the best ground truth for writing a Calimero app: every example is a real crate that builds to WASM with cargo mero build and has a native TestHost test suite under tests/ or in a #[cfg(test)] module. This page walks the ones worth reading first.

Each app follows the same shape (see SDK macros):

  • A single #[app::state] struct built from CRDT collections.
  • An #[app::logic] impl with an #[app::init] constructor, mutating methods, and read-only views.
  • Optionally an #[app::event] enum it emits.

Start with the state struct — it tells you which collection types the app leans on, and therefore which merge behavior you are getting for free. Then read the methods to see how those collections are driven. The walkthroughs below quote the real struct and list the real method names; open the linked src/lib.rs for the full bodies.

Every user-facing example below links to its source crate and to the page that documents the concept it teaches. The ones marked (walkthrough below) also have a full worked section further down this page; the rest are compact entries — the source is short enough to read end to end.

Example Teaches Documented in
kv-store A map of last-writer-wins values; events; entry / get_mut patterns (walkthrough below) Collections
kv-store-init Seeding initial state from an #[app::init] constructor SDK macros
kv-store-with-handlers Routing emitted events to handler methods; a CRDT Counter (walkthrough below) SDK macros
Example Teaches Documented in
kv-store-with-shared-storage SharedStorage: group-writable state guarded by a rotatable writer set Permissioned storage
kv-store-with-user-and-frozen-storage UserStorage (per-owner slots) + FrozenStorage (write-once, content-addressed) Permissioned storage
components-demo The access-control components: Ownable, AccessControl (named roles, grant_role / has_role), PermissionedStorage Permissioned storage
private_data #[app::private] node-local state that is never synced across the mesh Permissioned storage · State modeling
Example Teaches Documented in
sorted-kv-store SortedMap: a key-ordered map with range / prefix / pagination Collections
sorted-set-store SortedSet: an ordered add-wins set with range / prefix / pagination Collections
custom-key-store Custom key types via an AsRef<[u8]> newtype Collections
Example Teaches Documented in
collaborative-editor Concurrent collaborative text with the Rga CRDT (walkthrough below) Collections
nested-crdt-test Nesting collections inside collection values (maps of maps, vectors of counters) Advanced SDK
team-metrics-macro Nested CRDT structs via #[derive(Mergeable)] (walkthrough below) SDK macros
team-metrics-custom A hand-written Mergeable for merge logic the derive can’t express (walkthrough below) Advanced SDK
blobs Blob storage: hold file metadata + announce blobs to the context (walkthrough below) Blobs
xcall-example Cross-context calls + the #[app::xcall] entry-point gate (walkthrough below) Cross-context calls
Example Teaches Documented in
migrations Schema-versioning scenarios — adding, removing, renaming, and retyping fields across versions Migrations

The canonical starting point: a string→string key/value store where each value follows last-writer-wins. It also shows off the entry / get_mut access patterns and emits an event on every mutation.

State shape (apps/kv-store/src/lib.rs):

#[app::state(emits = for<'a> Event<'a>)]
pub struct KvStore {
items: UnorderedMap<String, LwwRegister<String>>,
}

The UnorderedMap<String, LwwRegister<String>> pattern is the bread and butter of Calimero state: an add-wins map whose values are each a last-writer-wins register, so concurrent writes to the same key resolve by timestamp while writes to different keys all survive a merge.

Key methods:

Method What it does
set(key, value) Insert or overwrite a key; emits Inserted or Updated.
update_if_exists(key, value) Mutate in place via get_mut only when the key exists; returns whether it landed.
get_or_insert(key, value) Read-or-create via the entry API; returns the resulting value.
get(key) / get_unchecked(key) / get_result(key) Three read variants: optional, panicking, and erroring (Error::NotFound).
entries() Collect all pairs into a BTreeMap<String, String>.
len() / remove(key) / clear() Count, delete one, delete all (Cleared).

Build & run. From the app directory, cargo mero build drops kv_store.wasm into res/. Then follow the Quickstart to install and call it:

Terminal window
meroctl --node node1 app install --path ./res/kv_store.wasm \
--package com.example.kvstore --version 1.0.0
meroctl --node node1 call set --context <context-id> \
--args '{"key": "hello", "value": "world"}'

Same key/value core as kv-store, but each set/remove/clear emits an event tagged with a handler method name — showing how an emitted event can be routed to a callback method. It also threads a CRDT Counter to count handler executions across all nodes.

State shape (apps/kv-store-with-handlers/src/lib.rs):

#[app::state(emits = for<'a> Event<'a>)]
pub struct KvStore {
items: UnorderedMap<String, LwwRegister<String>>,
handlers_called: UnorderedMap<String, LwwRegister<String>>,
handler_counter: Counter,
}

Key methods:

Method What it does
set / remove / clear Same as kv-store, but emit! pairs the event with a handler name (e.g. "insert_handler").
insert_handler / update_handler / remove_handler / clear_handler The handler callbacks; each logs and bumps handler_counter.
get_handler_execution_count() The global G-Counter sum of handler runs across nodes.
get_handlers_called() / get_unique_handlers_called() / was_handler_called(name) Introspect which handlers ran.
get / entries / len The same reads as kv-store.

Build & run. cargo mero build produces res/kv_store_with_handlers.wasm; install and call it as in the Quickstart.

A real-time collaborative text editor built on the ReplicatedGrowableArray (RGA) text CRDT, so multiple users can edit the same document concurrently and converge without manual conflict resolution.

State shape (apps/collaborative-editor/src/lib.rs):

#[app::state(emits = EditorEvent)]
pub struct EditorState {
pub document: ReplicatedGrowableArray,
pub edit_count: Counter,
pub metadata: UnorderedMap<String, LwwRegister<String>>,
}

Every field is a CRDT on purpose — the source notes that mixing non-CRDT fields into root state causes data loss under concurrent merges. metadata holds "title" and "owner" as LWW registers.

Key methods:

Method What it does
insert_text(position, text) Insert at an index; bumps edit_count, emits TextInserted.
delete_text(start, end) Delete a range; emits TextDeleted.
replace_text(start, end, text) Delete-then-insert in one call.
append_text(text) Insert at the current end.
clear() Delete the whole document.
set_title(new_title) / get_title() Edit / read the "title" metadata register.
get_text() / get_length() / is_empty() Read the rendered document.
get_stats() Formatted title / length / edit-count / owner summary.

Build & run. cargo mero build produces res/collaborative_editor.wasm. The collaborative payoff appears with two nodes editing the same context — install on each, then interleave insert_text / delete_text calls and read get_text() on both; see Quickstart for the install flow.

Two sibling apps that store nested CRDT structs as map values — a TeamStats of three Counters per team — and differ only in how that struct merges. They are the clearest illustration of #[derive(Mergeable)] versus a hand-written merge.

State shape (apps/team-metrics-macro/src/lib.rs):

#[derive(Debug, Default, Mergeable, BorshSerialize, BorshDeserialize)]
#[borsh(crate = "calimero_sdk::borsh")]
pub struct TeamStats {
pub wins: Counter,
pub losses: Counter,
pub draws: Counter,
}
#[app::state(emits = MetricsEvent)]
pub struct TeamMetricsApp {
pub teams: UnorderedMap<String, TeamStats>,
}

In team-metrics-macro, #[derive(Mergeable)] writes the merge for you — it just merges each field, which works because every field is itself a CRDT. The team-metrics-custom app has the identical state but writes impl Mergeable for TeamStats { fn merge(...) } (and a matching RekeyTarget) by hand, for when you need merge logic the derive can’t express — see Advanced SDK. Reach for the derive unless you have a reason not to.

Key methods (identical across both):

Method What it does
record_win(team_id) / record_loss / record_draw entry(team_id).or_default(), increment the matching counter, return the new total, emit an event.
get_wins(team_id) / get_losses / get_draws Read a team’s counter; errors if the team is unknown.

Build & run. cargo mero build produces res/team_metrics_macro.wasm (or team_metrics_custom.wasm); install and call per the Quickstart.

A minimal file-sharing app: the binary lives in Calimero’s blob store, while the app holds only metadata records keyed by a generated file id. Uploading also announces the blob to the context so other nodes can discover and download it.

State shape (apps/blobs/src/lib.rs):

#[app::state(emits = FileShareEvent)]
pub struct FileShareState {
pub owner: LwwRegister<String>,
pub files: UnorderedMap<String, FileRecord>,
pub file_counter: Counter,
}

FileRecord (id, name, blob_id, size, mime type, uploader, timestamp) is a plain struct with a hand-rolled Mergeable that treats each record as an atomic last-writer-wins value, using uploaded_at as the tiebreaker — a derive would have forced every field into its own CRDT wrapper. blob_id is the SDK’s BlobId newtype, which serializes to a base58 string over the wire.

Key methods:

Method What it does
upload_file(name, blob_id, size, mime_type) Mint a file_<n> id, env::blob_announce_to_context(...) the blob, store the record, emit FileUploaded.
delete_file(file_id) Remove the metadata record (the underlying blob is not deleted), emit FileDeleted.
get_file(id) / get_blob_id_b58(id) Fetch a record / just its BlobId for download.
list_files() / search_files(query) All records / case-insensitive name search.
get_total_files_size() / get_stats() Sum of file sizes / a formatted summary.

Build & run. cargo mero build produces res/blobs.wasm; install and call per the Quickstart.

A ping/pong across two contexts that demonstrates cross-context calls and, crucially, the xcall entry-point gate: only methods marked #[app::xcall] may be invoked from another context.

State shape (apps/xcall-example/src/lib.rs):

#[app::state(emits = Event)]
pub struct XCallExample {
counter: Counter,
secret_counter: Counter,
}

counter is bumped by pong (the declared xcall entry point); secret_counter is bumped by secret, which is deliberately not an xcall entry point — an xcall targeting it is denied by the node before it runs.

Key methods:

Method What it does
ping(target_context) env::xcall the target’s pong; emits PingSent.
ping_to(target_context, method) Ping an arbitrary method — used to show the node denying a non-xcall target.
pong(from_context) (#[app::xcall]) xcall-only: checks env::xcall_origin() matches from_context, then bumps counter and emits PongReceived.
secret(from_context) A normal method (not xcall-able) that bumps secret_counter.
get_counter() / get_secret_counter() Read each counter.

Build & run. cargo mero build produces res/xcall_example.wasm with the embedded ABI; install it into two contexts, then ping one toward the other and watch get_counter() move on the target. See Quickstart for the install flow.

A handful of crates under apps/ are harness and conformance fixtures, not teaching examples — they exist to exercise the node, the sync layer, or the wire formats, and read awkwardly as app tutorials. Reach for them only when you are working on the harness or the formats they pin down:

Fixture Role
e2e-kv-store End-to-end node test fixture.
sync-test State-sync / convergence harness fixture.
scaffolding-e2e Fixture for the project-scaffolding end-to-end tests.
abi_conformance Pins the app ABI format.
state-schema-conformance Pins the state schema format.
migration-harness-example Fixture for the migration harness.