Skip to content

Getting Started

This guide walks you from an empty directory to a running Calimero service written in TypeScript.

  • Node.js >= 18
  • pnpm >= 8 (npm works too)
  • A running Calimero node (merod) and the meroctl CLI to install and call your service
  • Linux or macOS — the build toolchain downloads a WASI SDK and QuickJS compiler that are not published for Windows (use WSL)

The SDK is split into two packages: the runtime library and the build CLI.

Terminal window
npm install @calimero-network/calimero-sdk-js
npm install -D @calimero-network/calimero-cli-js
# or with pnpm
pnpm add @calimero-network/calimero-sdk-js
pnpm add -D @calimero-network/calimero-cli-js

A Calimero service has two halves:

  • a state class marked @State — the data that is persisted and synced, and
  • a logic class marked @Logic(StateClass) — the methods clients call.

Create src/index.ts:

import { State, Logic, Init, View, createCounter } from '@calimero-network/calimero-sdk-js';
import type { Counter } from '@calimero-network/calimero-sdk-js/collections';
import * as env from '@calimero-network/calimero-sdk-js/env';
@State
export class CounterApp {
// Initialize CRDT fields inline with the factory helpers so the runtime
// reuses the persisted collection ID on every invocation.
count: Counter = createCounter();
}
@Logic(CounterApp)
export class CounterLogic extends CounterApp {
@Init
static initialize(): CounterApp {
env.log('Initializing counter');
return new CounterApp();
}
increment(): void {
this.count.increment();
env.log('Counter incremented');
}
@View()
getCount(): bigint {
return this.count.value();
}
}

Key points:

  • Initialize CRDT fields inline with the create* helpers (createCounter(), createUnorderedMap(), …). The logic class methods run with this bound to a hydrated state instance, so inline defaults guarantee the runtime reattaches the persisted collection ID instead of allocating a fresh one.
  • Mark read-only entry points with @View(). The dispatcher then skips the persistence pipeline for that method — no state snapshot, no delta, no gossip. A method without @View() is assumed to mutate state.

The calimero-sdk build command compiles your TypeScript through Rollup → QuickJS → WASI-SDK into a single WASM module, emitting an ABI manifest alongside it:

Terminal window
npx calimero-sdk build src/index.ts -o build/service.wasm

This produces build/service.wasm plus build/abi.json (the ABI manifest used for Rust-compatible serialization and for client generation).

Useful flags:

  • -o, --output <path> — output path (default build/service.wasm).
  • --no-optimize — skip the wasm-opt pass for faster dev builds. Optimization is on by default.
  • --verbose — show every pipeline stage (Rollup, QuickJS, Clang, optimization).

There is also calimero-sdk validate src/index.ts to check a source file before building.

Install the built WASM on a node and call its methods with meroctl:

Terminal window
# Install the service into a context
meroctl --node node1 app install --path build/service.wasm
# Call a mutating method
meroctl --node node1 call --context-id <CONTEXT_ID> --method increment
# Call a view
meroctl --node node1 call --context-id <CONTEXT_ID> --method getCount

For data that should stay on the executing node and not replicate through CRDT deltas (cached secrets, per-node bookkeeping), use the private storage helper:

import { createPrivateEntry } from '@calimero-network/calimero-sdk-js';
const secrets = createPrivateEntry<{ token: string }>('private:secrets');
// Read-or-initialize
const current = secrets.getOrInit(() => ({ token: '' }));
// Mutate in place (initialiser is used when no value exists yet)
secrets.modify(
value => {
value.token = 'rotated-token';
},
() => ({ token: '' })
);

PrivateEntryHandle also exposes get(), set(value), remove(), and getOrDefault(value). Entries are node-local — they never appear in CRDT deltas or gossip. See the Private data guide for the full API and when to use it.