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, ships a build.sh, 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.

ExampleTeachesDocumented in
kv-storeA map of last-writer-wins values; events; entry / get_mut patterns (walkthrough below)Collections
kv-store-initSeeding initial state from an #[app::init] constructorSDK macros
kv-store-with-handlersRouting emitted events to handler methods; a CRDT Counter (walkthrough below)SDK macros
ExampleTeachesDocumented in
kv-store-with-shared-storageSharedStorage: group-writable state guarded by a rotatable writer setPermissioned storage
kv-store-with-user-and-frozen-storageUserStorage (per-owner slots) + FrozenStorage (write-once, content-addressed)Permissioned storage
components-demoThe access-control components: Ownable, AccessControl (named roles, grant_role / has_role), PermissionedStoragePermissioned storage
private_data#[app::private] node-local state that is never synced across the meshPermissioned storage · State modeling
ExampleTeachesDocumented in
sorted-kv-storeSortedMap: a key-ordered map with range / prefix / paginationCollections
sorted-set-storeSortedSet: an ordered add-wins set with range / prefix / paginationCollections
custom-key-storeCustom key types via an AsRef<[u8]> newtypeCollections
ExampleTeachesDocumented in
collaborative-editorConcurrent collaborative text with the Rga CRDT (walkthrough below)Collections
nested-crdt-testNesting collections inside collection values (maps of maps, vectors of counters)Advanced SDK
team-metrics-macroNested CRDT structs via #[derive(Mergeable)] (walkthrough below)SDK macros
team-metrics-customA hand-written Mergeable for merge logic the derive can’t express (walkthrough below)Advanced SDK
blobsBlob storage: hold file metadata + announce blobs to the context (walkthrough below)Blobs
xcall-exampleCross-context calls + the #[app::xcall] entry-point gate (walkthrough below)Cross-context calls
ExampleTeachesDocumented in
migrationsSchema-versioning scenarios — adding, removing, renaming, and retyping fields across versionsMigrations

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:

MethodWhat 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, ./build.sh runs the load-bearing line cargo build --target wasm32-unknown-unknown --profile app-release and 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:

MethodWhat it does
set / remove / clearSame as kv-store, but emit! pairs the event with a handler name (e.g. "insert_handler").
insert_handler / update_handler / remove_handler / clear_handlerThe 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 / lenThe same reads as kv-store.

Build & run. ./build.sh 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:

MethodWhat 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. ./build.sh 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):

MethodWhat it does
record_win(team_id) / record_loss / record_drawentry(team_id).or_default(), increment the matching counter, return the new total, emit an event.
get_wins(team_id) / get_losses / get_drawsRead a team’s counter; errors if the team is unknown.

Build & run. ./build.sh 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:

MethodWhat 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. ./build.sh 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:

MethodWhat 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. ./build.sh 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:

FixtureRole
e2e-kv-storeEnd-to-end node test fixture.
sync-testState-sync / convergence harness fixture.
scaffolding-e2eFixture for the project-scaffolding end-to-end tests.
abi_conformancePins the app ABI format.
state-schema-conformancePins the state schema format.
migration-harness-exampleFixture for the migration harness.