Skip to content

JS SDK vs. the Rust SDK

Calimero apps can be written in Rust (the native calimero-sdk, part of calimero-network/core) or in TypeScript/JavaScript (this SDK). Both compile to a single WASM module that the node runs the same way. This page maps the two so you can move between them.

The short version: the programming model is the same — the same app structure, the same CRDT collections, the same events, the same ABI, and the same convergence guarantees. The differences are in language, tooling, and a few advanced features that the Rust SDK, as the reference implementation, gets first.

Both SDKs target one runtime, so most of what you learn transfers directly:

  • Same platform. Your code compiles to WASM and runs inside the node’s runtime, reaching the node only through the Calimero host functions. The storage layer, CRDT engine, causal DAG, P2P sync, and access control are identical — they live in the node, not the SDK.
  • Same app shape. A state type, a logic block with methods, an initializer, events, and read-only views — expressed with decorators in JS and attribute macros in Rust (table below).
  • Same CRDT collections. UnorderedMap, UnorderedSet, Vector, Counter, PNCounter, LwwRegister, Rga, SortedMap, SortedSet, AuthoredMap, AuthoredVector, UserStorage, FrozenStorage, and SharedStorage exist in both, with the same names and the same convergence semantics. See CRDT Collections.
  • Same convergence. Concurrent writes converge by the same rules on both — last-write-wins by timestamp, additive counters, RGA interleaving, owner-gated attributed entries — because the merge happens in the node, not the SDK.
  • Same ABI, so clients don’t care. Both emit the mandatory calimero_abi_v1 manifest, and values use Calimero’s Borsh encoding. A generated client works the same against a JS or a Rust service, and a state written under one is readable by the other given the same schema — which is what makes migrating a Rust app to JS (or back) possible.
Concept Rust SDK (calimero-sdk) JS SDK (@calimero-network/calimero-sdk-js)
Language / target Rust → native wasm32 TypeScript/JS → QuickJS compiled to WASM
Build cargo mero build calimero-sdk build (Rollup → QuickJS → WASM)
State type #[app::state] @State
Logic / methods #[app::logic] impl @Logic class
Initializer #[app::init] @Init
Events #[app::event] + app::emit! @Event + emit()
Read-only methods &self methods @View()
Method result app::Result<T> return a value, or throw
Serialization borsh derive ABI-driven serialize/deserialize (Borsh-compatible)
Collection ids assigned via the state macro new Collection() + runtime deterministic-id assignment
Custom conflict rules custom CRDT merge via a WASM callback @Mergeable structs
  • Language and tooling. Rust gives you a compiler-checked type system, cargo, and crates; JS/TS gives you a faster edit loop and the npm ecosystem. App structure is expressed with attribute macros in Rust and decorators in JS, but they mean the same thing.
  • Performance and size. A Rust service compiles to compact native WASM. A JS service ships the QuickJS interpreter inside its WASM and interprets your bytecode, so it is larger and slower per call. For most collaborative apps sync and storage dominate, so this rarely matters; for hot, compute-heavy paths Rust is the better fit.
  • How the root document converges (internals). Both converge, but through different machinery: a Rust root is a native structure the merge engine walks in-guest, while a JS root is opaque to the node and opts into a host-invoked guest merge (__calimero_merge_root_state). You write this code in neither SDK — but it is what shapes the @Mergeable limitation below. See the architecture for the JS data flow.

The Rust SDK is the reference implementation; the JS SDK tracks it, and a few capabilities are not exposed yet. Know these before you build:

  • Custom-struct conflict resolution is scoped (@Mergeable). Field-aware and custom merges run only for a struct that is a direct field of your @State root — there, every replica re-runs your merge during sync. A @Mergeable struct stored inside a CRDT collection (a map value, a register payload) crosses to the host as opaque bytes, so concurrent edits to it on other nodes resolve last-write-wins, and a custom handler does not replay there. Rust re-runs a registered structural merge on every node regardless of nesting. For guaranteed convergence in JS today, model shared state with the built-in CRDT collections rather than custom structs. See Mergeable structs.
  • SharedStorage is a single value. JS supports create / set / get / writers / writableByMe / isFrozen / rotateWriters. It does not yet expose per-writer capabilities (WRITE/DELETE/ADMIN op masks, scoped rotation) or SharedStorage<Collection> nesting — all of which Rust has.
  • Little access control beyond a writer set. JS gets one concrete membership primitive — SharedStorage, where any writer may do anything. Rust has a whole authorization layer with no JS equivalent: Ownable (a single-owner cell with authenticated transfer_ownership), AccessControl (role-based access — grant/revoke roles, has_role, members-of, roles projected onto per-writer capabilities), per-operation OpMask capabilities (WRITE / DELETE / ADMIN, so a writer can be allowed to write but not delete), and the generic PermissionedStorage<T, Acl> + pluggable Authorizer policies (guard/can). Model authorization in your JS app logic instead.
  • No app-version migrations. Rust can upgrade a deployed app and migrate its existing state: #[app::migrate] defines a migration entrypoint the runtime runs on upgrade, #[app::migration_check] validates the migrated state (and can abort, leaving the old state intact), with built-in invariant helpers (entity-count parity, no orphaned refs, conservation) and a state version. The JS SDK has no equivalent — no migration entrypoint, no migration check, and emit_migration_witness is not wrapped — so changing a JS app’s state schema on an existing context isn’t supported through the SDK yet. Evolve schemas defensively (additive/optional fields) or redeploy with fresh state. (This is unrelated to the Migrating from Rust guide, which is about porting a Rust service to TypeScript.)
  • Fewer app-lifecycle and cross-context hooks. Rust has #[app::destroy] (a teardown method run when a context/app is torn down) and #[app::xcall] (mark a method as a cross-context entry point and restrict who may call it). JS can schedule an outbound xcall, but cannot declare a method as an xcall target, restrict its callers, or read the caller’s origin context (xcall_origin) to authorize it — and it has no teardown hook.
  • Counter is grow-only. JS Counter only increments — use PNCounter when a value also goes down. (Rust splits GCounter / PNCounter.)
  • Borsh is a JS reimplementation. JS encodes/decodes Borsh in TypeScript to match Calimero’s ABI rather than using Rust’s canonical borsh crate. State interoperates as long as both sides share the same schema — avoiding a schema mismatch is the app author’s responsibility.
  • Smaller gaps. JS methods return a value or throw rather than Rust’s typed app::Result<T, E> (with err! / bail!); logging is a flat env.log rather than leveled tracing; and @View() is a runtime marker only — it is not emitted as read-only intent in the ABI, so the node treats a JS view method as write-intent (it still works, but skips the read-lock concurrency optimization Rust view methods get).

Neither SDK can manage context membership, lifecycle, or governance from service code (those host functions are not exposed to any guest) — that is a platform-level boundary, not a JS-vs-Rust difference. Do it through the client or CLI.