Skip to content

Your app's interface (ABI)

Every Calimero application carries an ABI (Application Binary Interface): a typed, machine-readable description of its public surface. The ABI is your app’s contract with the outside world — it tells clients, frontends, and meroctl exactly which methods exist, what arguments they take, what they return, which errors they can raise, what events they emit, and how state is shaped and versioned.

You never hand-write the ABI. It is generated from your annotated source at build time and travels inside the compiled WASM, so it can never drift from the code that ships.

The ABI is a Manifest (defined in crates/wasm-abi/src/schema.rs). At the top level it has:

FieldMeaning
schema_versionABI format version — currently wasm-abi/1.
typesNamed complex types (records, variants, byte arrays, aliases) referenced by $ref elsewhere in the manifest.
methodsThe callable entry points (see below).
eventsEvents the app may emit, each with an optional payload type.
state_rootThe name (in types) of the persistent state struct.
state_versionThe schema generation this build targets — AppState::SCHEMA_VERSION from #[app::state(version = N)], default 1.
migrationsDeclared migration edges, one per retained from → from+1 hop.

Each method records its full call shape:

{
"name": "opt_record",
"params": [
{ "name": "p", "type": { "$ref": "Person" }, "nullable": true }
],
"returns": { "$ref": "Person" },
"returns_nullable": true
}
  • params — ordered argument list, each with a name and a type. A nullable flag marks Option<T> arguments.
  • returns / returns_nullable — the return type and whether it is optional.
  • errors — the typed error codes a method can return (omitted when empty).
  • intentread_only for methods marked #[app::view], otherwise treated as mutating. The node uses this to pick a shared read lock vs an exclusive write lock. Absent on older modules (treated as a write, the fail-safe).
  • xcall_callabletrue for methods marked #[app::xcall], the cross-context entry points.

Argument and return types are either inline scalars (bool, i32, u64, string, bytes, unit, …), inline collections (list, map, record), or a $ref to a named entry in types. This is enough for a caller to build a correctly typed request and decode the response without ever reading your Rust.

{ "name": "Named", "payload": { "kind": "string" } }

Each event has a name and an optional payload type — the shape a subscriber receives when the app emits it.

The state portion of the manifest — state_root, the types it transitively references, state_version, and migrations — describes how the app’s persistent CRDT state is laid out and how it evolves across versions. A migrations entry names the migrate entry point and the version it carries state from:

{ "method": "migrate_v1_to_v2", "from_version": 1 }

Retaining several edges lets a node that is multiple versions behind replay v1→v2 then v2→v3 rather than jumping straight to the latest hop.

Generated from your code, embedded in the bytecode

Section titled “Generated from your code, embedded in the bytecode”

The ABI is derived from the same #[app::*] macros that define your runtime surface — see SDK macros. Marker attributes like #[app::view] and #[app::xcall] are no-ops on the generated code; they exist so the build can read them off your methods and record them in the manifest. Because the manifest is produced from your annotated source at build time, it cannot describe a method your code doesn’t have, or omit one it does.

The state-schema subset of the manifest is then written into the WASM as a custom section named calimero_abi_v1 (crates/wasm-abi/src/embed.rs). Two consequences follow:

  • It’s covered by the bytecode hash. A blob is content-addressed by its bytes, and the section is part of those bytes — so the embedded schema is fixed to the exact build it ships in. There is no separate, forgeable side-channel.
  • The node can read it back from any installed blob, with no external file. The read path is fail-open: a missing or malformed section is treated as absent.

A frontend, client, or meroctl reads the ABI to learn the callable surface without any out-of-band knowledge of your app. The embedded section can be pulled straight out of a compiled module with the calimero-abi tool:

Terminal window
# Extract the full ABI manifest from a compiled app
calimero-abi extract app.wasm --output abi.json
# Or inspect just the state schema embedded in the bytecode
calimero-abi state app.wasm

From there a client walks methods to build typed requests — picking the method by name, encoding each param according to its type, and decoding returns on the way back — and walks events to type the notifications it subscribes to. Named types are resolved through types by following $ref.

Because the state schema is embedded in every blob, the upgrade machinery reads it directly from the bytecode rather than trusting any external metadata. When a new version is installed, the no-silent-downgrade rail compares the old and new embedded schemas to catch unsafe changes — for example replacing an identity-gated collection (which carries per-entry authorship or a writer ACL) with a plain type that would silently strip that provenance. The same check runs in CI (calimero-abi diff) and at the node’s upgrade gate, both reading the embedded ABI. The migration edges declared in the manifest are what the gate replays to carry state forward.

In other words, the ABI isn’t just documentation for callers — it’s the load-bearing record the protocol itself uses to reason about your app across versions.