---
sdk = "node-bindings"
previous_release_version = "1.11.0-dev"
---

# Node SDK 1.11.0

## Highlights

- **`client.catchUpToLive()`** — a bounded, one-shot sync for agents that poll for new state instead of holding a live stream.
- **`client.close()`** for clean shutdown, so deleting the database file no longer races detached workers.
- **Idempotency keys on sends**, so a retried send produces the same message ID instead of a duplicate.
- **OpenTelemetry export of both spans and logs**, plus a standalone `initLogging()` you can call before creating a client.
- **Background worker tuning** via `workerConfig` — per-worker intervals, jitter, and enable/disable.
- **Single-connection database mode** for services running many clients in one process.

This release contains breaking changes. Read the Breaking Changes section before upgrading.

## What's Changed

The two API connections each client used to open have been collapsed into one, and `isSecure` is gone from the connection path. Logging moved onto a dedicated pipeline that can export spans and logs over OTLP, with a separate stdout level so a log shipper does not duplicate what OTLP already carries. Every exported binding function now emits an error-case span, so failures are attributable per call.

On the messaging side, sends accept an idempotency key; the top-level `send*` helpers take an options object instead of a bare boolean; and `updateAppData` takes an options object so future fields can be added without another break. Groups gained `proposalsEnabled()` and a `conversation.unstable.enableProposals()` entry point for the in-progress migration to proposal-based metadata updates.

The binding also switches to mimalloc as its global allocator and installs the rustls crypto provider explicitly, which fixes a device-sync panic on platforms where the previous static constructor did not run.

## Breaking Changes

### `createClient` drops `isSecure` and both create functions gain `workerConfig`

**This is a breaking change.**

Two positional changes to `createClient`:

- The `isSecure: boolean` third argument is **gone**. Every parameter after it shifts left by one.
- A `workerConfig?: WorkerConfigOptions` parameter was inserted between `deviceSyncWorkerMode` and `logOptions`.

Before: `(v3Host, gatewayHost, isSecure, db, inboxId, accountIdentifier, deviceSyncWorkerMode, logOptions, …)`

After: `(v3Host, gatewayHost, db, inboxId, accountIdentifier, deviceSyncWorkerMode, workerConfig, logOptions, …)`

`createClientWithBackend` gains the same `workerConfig` parameter in the same position (pass `undefined` for previous behavior). The separate `syncApi` client argument has been removed from the internal wiring — one API client now serves both paths.

### The top-level `send*` helpers take a `SendOpts` object instead of `isOptimistic`

**This is a breaking change.**

```ts
// Before
await conversation.sendText("hello", true);

// After
await conversation.sendText("hello", { optimistic: true });
```

This applies to `sendText`, `sendMarkdown`, `sendReaction`, `sendReadReceipt`, `sendReply`, `sendAttachment`, `sendRemoteAttachment`, `sendMultiRemoteAttachment`, `sendTransactionReference`, `sendWalletSendCalls`, `sendActions`, and `sendIntent`.

### `updateAppData` takes an options object

**This is a breaking change.**

```ts
// Before
await conversation.updateAppData("payload");

// After
await conversation.updateAppData({ value: "payload" });
```

### `registerIdentity` takes an optional second argument

`registerIdentity(signatureRequest, visibilityConfirmationOptions?)`. Existing single-argument calls continue to work.

### `prepareMessage` takes an optional third argument

`prepareMessage(encodedContent, shouldPush, idempotencyKey?)`. Existing two-argument calls continue to work.

### `getConversationById` validates the group ID

Group IDs are now fixed-size values. A hex string that does not decode to a valid group ID is rejected with an error rather than producing a conversation handle that fails later.

## New Features

### `client.catchUpToLive()`

Bring the local store current with the network, then stop — a bounded, one-shot sync for agents that poll rather than hold a live stream.

```ts
const summary = await client.catchUpToLive();
console.log(summary.messages, summary.conversations, summary.completed);
```

Counts are plain numbers (not `BigInt`). Idempotent: everything processed is persisted and catch-up does not advance durable message cursors, so a later call resumes from durable state and one with nothing owed reports zero. There is no caller deadline; the run is internally bounded by capped retries with backoff and a wire watchdog. Race it against your own timer if you need a hard wall-clock bound.

### `client.close()`

Cancel in-flight workers and streams, then release the database connection. Idempotent. `await` this before deleting the SQLite file or dropping the client reference to avoid late log spew from detached workers firing against a dead database.

### Idempotency keys on sends

`SendMessageOpts` and the new `SendOpts` both accept `idempotencyKey`. Re-sending identical content with the same key produces the same message ID and is deduplicated, so a retry after a timeout does not create a second message. Defaults to a timestamp when omitted.

### OpenTelemetry span and log export

`LogOptions` gained:

- `otelEndpoint` — e.g. `"http://collector:4317"`. Spans **and** `tracing` events (as correlated logs) are exported via OTLP.
- `resourceAttributes` — attributes attached to all exported telemetry, e.g. `{ "service.instance.id": "worker-7" }`.
- `stdoutLevel` — level for the stdout console layer only. Defaults to `level`. Set it to `warn` to quiet stdout below the OTLP export level so a log shipper does not duplicate logs already exported.

Two new process-global functions:

- `initLogging(options?)` — install the logging pipeline before any client exists. It is `async`, so `await` it. Idempotent; the first call wins, so it is safe to call this early and still pass `logOptions` to `createClient`.
- `flushTelemetry()` — flush buffered spans on graceful shutdown. No-op if telemetry was never enabled.

### Background worker configuration

`WorkerConfigOptions` — passed to `createClient` / `createClientWithBackend` — lets you tune the background worker scheduler:

```ts
const workerConfig = {
  defaultIntervalNs: 5_000_000_000n,
  workerIntervalsNs: [{ kind: "DeviceSync", intervalNs: 30_000_000_000n }],
  workerJittersNs: [{ kind: "CommitLog", jitterNs: 1_000_000_000n }],
  disabledWorkers: ["DisappearingMessages"],
};
```

Worker kinds: `DeviceSync`, `DisappearingMessages`, `KeyPackageCleaner`, `CommitLog`, `TaskRunner`. Omitting the whole object preserves default behavior.

### Single-connection database mode and pool sizing

`DbOptions` gained `useSingleConnection`. When `true`, the native database uses one connection (one file descriptor) instead of a pool, and `maxDbPoolSize`/`minDbPoolSize` are ignored. Intended for services running many clients in one process.

### `client.waitForRegistrationVisible(options?)`

Wait until a quorum of nodes confirms this client's identity update and key package are visible, so you do not race a just-registered installation against propagation. Configurable with `quorumPercentage`, `quorumAbsolute`, and `timeoutMs` (default 30 000 ms). Also available inline as the second argument to `registerIdentity`.

### Group metadata migration APIs

- `conversation.proposalsEnabled()` — whether this group has migrated to proposal-based metadata updates.
- `conversation.unstable.enableProposals({ force?, minVersion? })` — stage the migration. One-way and irreversible; hard-fails if any member's latest key package does not advertise the required proposal type. The `.unstable` namespace is the deliberate opt-in and will break at compile time when the API graduates.

### Error-case tracing on every exported function

Every `#[napi]` function now records a span on the error path, so a failure is attributable to a specific binding call in your traces.

### gRPC keepalive is tunable via environment variables

For deployments on high-latency paths where the default fast keepalive causes spurious `UNAVAILABLE` disconnects. Defaults are unchanged.

| Variable | Default |
| --- | --- |
| `XMTP_GRPC_KEEPALIVE_INTERVAL_SECS` | 16 |
| `XMTP_GRPC_KEEPALIVE_TIMEOUT_SECS` | 10 |
| `XMTP_GRPC_TCP_KEEPALIVE_SECS` (0 disables) | 16 |
| `XMTP_GRPC_KEEPALIVE_WHILE_IDLE` | true |

### Opt-in stream-liveness watchdog

A subscription whose transport wedges open — an L7 proxy that keeps answering HTTP/2 pings while the backend subscription is gone — used to deliver neither an error nor a close, and the consumer just hung. There is now an idle timer per stream that, when nothing arrives within the timeout, tears the stream down and re-subscribes from the persisted cursor.

It is **off by default**: with no env vars set, streams behave exactly as before. Long-lived agents can opt in.

| Variable | Default |
| --- | --- |
| `XMTP_STREAM_WATCHDOG_ENABLED` | false |
| `XMTP_STREAM_WATCHDOG_IDLE_TIMEOUT_SECS` | 300 |
| `XMTP_STREAM_WATCHDOG_RECONNECT_BASE_SECS` | 1 |
| `XMTP_STREAM_WATCHDOG_RECONNECT_JITTER_MS` (0 disables) | 1000 |

Because there is no server-side keepalive on this path yet, the idle timeout is deliberately long — a healthy but dormant stream will also trip and reconnect roughly once per timeout. Values are read once per process at first use.

## Bug Fixes

- **Device sync panic on some platforms.** The rustls crypto provider is now installed explicitly instead of relying on a static constructor that does not run everywhere (notably Apple targets), which caused a "No provider set" panic in the device-sync worker's HTTP client.
- **Crash on the welcome stream** after a group's creator left the group and was later re-added.
- **False-positive fork detection** after a member was removed from a group.
- **Missed messages in streams** after a recoverable error: the stream resumed past envelopes it had not yet delivered.
- **Cache read returning wrong rows.** A pairwise filter in the database read cache was expanding into a cross product, so a batch lookup could return unrelated rows.
- **Intentional transaction rollbacks** are no longer reported as telemetry errors.
- **Database disconnect errors** now propagate out of workers to the supervisor instead of being swallowed.
- **Dropped group invites for members using smart contract wallets.** A transient failure while verifying an ERC-1271/6492 signature was classified as permanent, so the welcome cursor advanced and the invite was skipped for good. Those failures are now retryable and the welcome is reprocessed on the next sync.
- **New installations reliably added to existing groups.** Reconciliation after a welcome now runs as a durable, retried task instead of one inline attempt whose per-group failures were swallowed, so a lost race no longer permanently skips a group.
- **Downgrade resilience for group intents.** Intent rows written by a newer version are now skipped rather than failing every intent query for that group, so downgrading to this version no longer wedges outbound messages in an affected group.
- **Archive export dropped migrated groups.** Groups that had migrated to proposal-based metadata were silently omitted from archives instead of exported.
- **Publish rollback.** A failed publish now resets the intent to `ToPublish` instead of leaving it stranded, and swallowed intent-error causes are preserved in `SyncSummary` rather than replaced with a generic error.
- **Membership adds during identity propagation.** `MissingSequenceId` is now retryable, so adding a member whose identity update has not fully propagated succeeds on retry instead of failing.
- **Groups stuck below the protocol version floor** now recover automatically once the client upgrades past the required floor, instead of staying paused.
- **`GetNodes` DNS flakes** are now retried instead of failing the call.
- **Reduced log noise** across retry, sync, welcome, and worker paths — steady-state and per-item logs moved from info/error to debug/warn, and duplicate database pool-failure spam is deduplicated across workers.
