Skip to content

Application execution

An application is a calimero:app component: a WASM component, built with calimero-sdk, that defines a state machine: its data types and the methods that change them. An execution (a run) is one call of one method. On a cluster, only the app group’s leader runs calls; the other replicas apply what the run produced (Replication).

The node runs components on calimero-app-engine-wasmtime through the host core calimero-app-host (crates/node/src/node/component.rs, crates/node/src/component.rs), preparing each bundle once and caching the compiled artifact by its hash. A WASM core module (an SDK 0.12 app, which ran on wasmer) is refused at upload (400 UNSUPPORTED_BUNDLE); an app of that kind that a data directory still holds is listed and can be deleted, and anything else on it answers 410 UNSUPPORTED_ENGINE.

A run reads the app’s state as an ordered, immutable version (StateView); its writes stay in the run. When it ends, they become the outcome’s diff, which every replica applies. If the run trapped or the app returned an error, the diff is empty and the entry is still logged with its status (422 APP_ERROR with details.{code, fields}, 422 TRAP with details.{kind, detail}).

The calimero:app world (crates/app-wit/wit/calimero-app.wit):

Interface What it does on a cluster
storage The app’s replicated state: get, put, delete, the commutative add, ordered range
context The caller, the app’s id and owner, its organization, the caller’s role and teams there, the applied index, unique ids, and starting an action
events Emit an event to an audience; a removed member stops receiving at once
blobs, files The app’s blobs and sealed files
search The app’s own full-text indexes, in views
xcall Send a call into another app (outbox), or call or view one synchronously
action Inside an action: its key, and views and calls of the app
wasi:clocks, wasi:random The leader’s wall clock and randomness, read once: followers apply the result
wasi:http Outbound HTTPS, from actions and as one recorded request of a call (below)

A component has one space: its own keys, which the node keeps as hidden rows of the app’s group, sealed on disk. Per-caller data is rows keyed by env::caller(), and writer sets are rows the app checks before it writes (apps/kv-store-with-shared-storage); the host keeps no space of its own inside an app’s keys. (SDK 0.12 apps had a private and a host space beside the shared one; they are gone with that SDK.)

Before a run’s outcome becomes an entry, the node applies these host-side rules:

  • Read-only team members: a member whose row in the app’s team is read-only reaches the app’s views only: a call to any other method is refused with 403 before it runs, so no run burns the developer’s fuel uncounted.
  • Entry size: an entry over the log’s cap is logged trapped (HostDenied), and arguments over 512 KiB are 413 ARGS_TOO_LARGE.
  • Conflicts: a run built on a state another entry has since moved past, or on a bundle an upgrade replaced, commits as a no-op and runs again.

A component sends HTTPS from an action, or as one recorded request of a call (both below). Either way the request leaves only when every gate holds:

  1. the operator turned outbound on and allows the host ([outbound] in the node’s configuration);
  2. the app’s bundle is a signed release whose manifest declares a URL prefix covering the request;
  3. that prefix was approved for the publisher by whoever the cluster’s governance gives outbound approvals;
  4. never from a view, init, a migration, or a read-only member’s call;
  5. https on port 443 to a name that resolves only to public addresses, within the size limits, no redirects.

A calimero:app component calls out from an action (spec app-runtime-v0.14 D12, S9): an #[app::action] method that runs outside the log, on the app’s leader. A call can also make one recorded request itself (below), never a view.

  • Start (D17). A client calls the action’s method through /intents, or a call or timer runs env::start_action(method, &args). Either way an entry logs that it started, under a key (a.<term>.<seq>.<n>), and the answer is { "actionId" }.
  • Run. The leader runs pending actions in a bounded pool, with a 10 s budget and the starter as caller (D13). An action has no storage. It reads with ctx.view, writes with ctx.call, and sends HTTP with ctx.fetch over wasi:http. Each ctx.call is its own log entry under a key derived from the action’s key, the method, the arguments’ hash and an occurrence count (D27), so a retried action’s calls replay. A method marked #[app::allow(action)] is reachable only from the app’s own actions.
  • Outbound (S9, R14). A request leaves only through the same gate as above. The URL must sit under a prefix the signed release’s manifest declares, the owner must have approved it, the operator’s hosts apply, and there is one request per run. {{secret:NAME}} is filled in by the node and redacted from the response. Bodies are bounded in-memory buffers, so a guest cannot block a node thread.
  • End (D22). Success, or an app error, a fault or out-of-fuel, ends the action at once. A timeout is retried with backoff, 5 attempts in all. The end is logged. A failure sends an ActionFailed { actionId, method, reason } event to the starter and runs the app’s onActionFailed as the starter. A new leader resumes every action that has not ended, with the same key.

A component’s call, timer or xcall delivery may send one request itself (env::fetch; spec D76), for a check that belongs in the same request as the write it guards, such as a captcha service verifying a Turnstile token:

  • The same gate as actions. A URL the signed release declares under outbound, the owner approved and the operator allows; {{secret:NAME}} is filled in by the node and redacted from the response. Views, init and migrations cannot send (HostDenied), nor can a read-only member’s call.
  • Once, recorded. The leader sends it while it runs the call, before proposing. The call’s log entry records the exchange (Outcome::http: the request as the app made it, with placeholders, and the response the gate returned; bodies cut to 64 KiB), whatever the call’s status. Followers apply the diff and never fetch, and a retry with the same idempotency key replays the recorded answer. A run the log never commits (a leader race, a timeout) recorded nothing and sends again when it re-runs.
  • Bounded. The node waits at most 1.5 s for the response, inside the call’s 2 s budget; a timeout, a refusal or an unreachable host is an error the app’s fetch returns.
  • Trust. The leader sees the response before it is logged. Attestation covers the code that made the request and its TLS connection, not the remote server’s answer: the same trust as an action’s request.

apps/v2-fixtures/captcha’s verifyTurnstile is the example; the kv fixture’s setHumanTurnstile reaches it with a synchronous xcall, so the token is checked and the form written in one request.

Only the leader runs, so the clock, randomness, outbound responses and search answers cannot make replicas disagree. Runs are still bounded and transactional: resources are capped (Limits), and a trap or an app error leaves an empty diff.

A view runs the same way against a replica’s applied state, with search available, and its writes are discarded (Reads). An upgrade swaps the bundle; a named migration is refused MigrationsNotSupported (Upgrades).