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.
What’s the same
Section titled “What’s the same”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, andSharedStorageexist 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_v1manifest, 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.
Side-by-side
Section titled “Side-by-side”| 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 |
What differs (fundamentals)
Section titled “What differs (fundamentals)”- 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@Mergeablelimitation below. See the architecture for the JS data flow.
Limitations of the JS SDK
Section titled “Limitations of the JS SDK”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@Stateroot — there, every replica re-runs your merge during sync. A@Mergeablestruct 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. SharedStorageis 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) orSharedStorage<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 authenticatedtransfer_ownership),AccessControl(role-based access — grant/revoke roles,has_role, members-of, roles projected onto per-writer capabilities), per-operationOpMaskcapabilities (WRITE / DELETE / ADMIN, so a writer can be allowed to write but not delete), and the genericPermissionedStorage<T, Acl>+ pluggableAuthorizerpolicies (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 stateversion. The JS SDK has no equivalent — no migration entrypoint, no migration check, andemit_migration_witnessis 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 outboundxcall, 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. Counteris grow-only. JSCounteronly increments — usePNCounterwhen a value also goes down. (Rust splitsGCounter/PNCounter.)- Borsh is a JS reimplementation. JS encodes/decodes Borsh in TypeScript to
match Calimero’s ABI rather than using Rust’s canonical
borshcrate. 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
throwrather than Rust’s typedapp::Result<T, E>(witherr!/bail!); logging is a flatenv.lograther 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.
See also
Section titled “See also”- Architecture — the QuickJS ↔ host runtime and build pipeline in detail.
- CRDT Collections — every collection type and its convergence behavior.
- Migrating from the Rust SDK — moving an app between the two.