Skip to main content
These guides target v0.33.0. Pin the package while qualifying your application and review the versioned upgrade guide before changing it.

Upgrade procedure

  1. Record the currently deployed package and native-library versions.
  2. Preserve a supported backup of identity and application state.
  3. Build against the target release and resolve changed APIs.
  4. Test restart, pending work and mixed-version peer behaviour on representative devices.
  5. Release to a limited cohort and inspect failures before expanding.
A previous binary is not automatically a safe rollback for newer persisted state. Follow the release’s storage compatibility guidance. Source on main can contain unreleased features; do not combine generated bindings from one revision with a native library from another.

Runtime changes to qualify

In v0.26, delivery state survives restart, the pending TTL changes from 30 minutes to 24 hours, and deduplication defaults become 2,000 entries retained for 24 hours. Account for the larger retention window in disk and retry budgets. In v0.27, relay group traffic reaches phones. Test group delivery over the relay after upgrading; compilation alone does not exercise these paths.

Upgrade from v0.32

No API is removed. The SDK’s UPGRADING §31 lists each change below.
  • Resends cross every hop. A resend that reaches a neighbor a full acknowledgement timeout or more after the last generation moves to the next one, at most three, carried in the reserved metadata key __resend, and forwarders on v0.33.0 suppress each generation separately. Generations travel as far as the upgraded devices on the path, so upgrade the devices that relay as well as the ones that send.
  • Receipts have their own relay allowance. A link’s acknowledgements, the frames carrying ack_for, have an allowance of half of peerBurst and peerRatePerSec, and use the ordinary one after it. A neighbor can be admitted at up to one and a half times the configured burst and rate; to keep those settings as a hard ceiling, set them a third lower. The device’s forwarding budget is unchanged.
  • Each delivery is answered once. A recipient answers a relayed copy unless it answered that message under the same generation in the last five seconds. A receipt re-drives waiting messages at once for a peer parked as unreachable and sends the rest one acknowledgement timeout later, spaced out, and a resend whose earlier copy is still in a local carrier queue waits one more rung. Tests that count frames, or time a resend after a receipt, take the new numbers.
  • A device’s own frame goes to four neighbors, one more than a forward, and the strongest link on each carrier gets a slot before any carrier gets a second.
  • Session probes back off for absent peers. A pending 1:1 session the mesh carries is probed from 15 seconds up to 600, and again at once when the peer is discovered.
  • Python: each phone on the Bluetooth peripheral appears once, under the address its hello proved. Key on that address in neighbor_discovered and the peer list.
  • Browser: paced Bluetooth writes. The worker keeps at most four unwritten fragments per link with the page, a sink’s write may return a promise, and a write pending for ten seconds closes its link. A custom sink that takes longer passes writeTimeoutMs to BluetoothCarrier.
  • React Native: development warnings. With __DEV__ set, the SDK warns when a peer id is not an off1… address and when calls come in the wrong order. No call is blocked and a release build logs nothing. A test suite that fails on console.warn sets devPeerAddressWarnings: false and devLifecycleWarnings: false, or fixes what they name. See development warnings.
  • ProtocolEngineHost hosts: warning diagnostics. The Flutter plugin in a debuggable Android build, and an iOS host built with DEBUG, receive the ordering hints through onDiagnostic at level warning. On Android the constructor adds integratorLifecycleWarnings, off by default, before onDiagnostic: pass onDiagnostic by name or as a trailing lambda, or add the new argument.

What v0.33.0 adds

The v0.33.0 changelog has every change.

Upgrade from v0.31

Nothing is removed from any binding. Three additions break code that names every case or every field, or implements a generated interface by hand, and the rest compiles unchanged and behaves differently. The SDK’s UPGRADING §30 lists every change. Also check these run-time changes:
  • A registered service can be asked for a subscription. Subscriptions are on by default in every binding; in Rust, service_subscriptions.enabled = false turns them off. Under the default restricted policy a peer’s request raises service_subscribe_received and waits for acceptServiceSubscription or rejectServiceSubscription; a request nobody answers is rejected after 60 seconds, so an app that ignores the event serves nobody. The device also advertises the capability in its signed key package. See service subscriptions.
  • A send that begins with __MEMBER__ is refused. The prefix is reserved for application membership, so sendMessage and every other public send fail with InvalidArgument on content that starts with it, as they already did for the other reserved prefixes.
  • Frames for a live mesh neighbour stay on its link. Acknowledgements, retries and session control frames for a peer with a live direct Bluetooth LE or Wi-Fi link leave on that link, as application messages already did, so a device with a relay configured sends fewer frames through it.
  • Android: a second SDK app on the phone takes another LAN port. The first app still listens for peer streams on port 8988. A second app that finds it taken binds a free port, advertises that one, and joins Wi-Fi Direct groups rather than forming them. A network policy that allows only 8988 blocks the second app’s inbound streams.
  • Bluetooth LE keeps discovering in the background and with the screen off. A backgrounded iPhone scans for the mesh service, so it discovers new peers and raises neighbor_discovered in the background. An Android phone with its screen off scans with a mesh service filter. Android also drops and redials a client link that takes writes and has never been answered, about 20 to 25 seconds after its first write, so a peer whose app restarted is reached again.
  • React Native: four calls work as documented again. On Android, sendReadReceipt sends instead of throwing. On iOS, acceptConnectionRequest and sendConnectionRequest accept an omitted keyPackage, and createInvite(name, true) signs the invite. On both platforms, checkInternetPresence(id, { force: true }) goes to the network each time. An app that worked around any of these can drop the workaround.
  • Smaller changes. mls_get_pending_welcome returns nothing for a 1:1 session after delete_session. An event callback may call set_event_callback, and a callback that throws no longer costs the polling queue an event. On Android, the native module’s start() rejects when no protocol has been created.

What v0.32.0 adds

The v0.32.0 changelog has every change.

Upgrade from v0.30

Nothing is removed from any binding. Four additions break code that names every case or every field, and the rest compiles unchanged and behaves differently. The SDK’s UPGRADING §29 lists every change. Also check these run-time changes:
  • Sessions heal themselves. A decrypt failure on a 1:1 session now sends a probe, at most once per peer every 30 seconds. When the two sides hold different groups, the side with the greater address gives up its group, emits convergence_diag with stage session_split_detected, and the other side resends its Welcome. A key package now also says when its sender holds no session, and the side that still holds one resends its Welcome or rebuilds. Both need the two devices on v0.31.0: a pair split across versions stays split until both update.
  • Nostr: a send to a stranger asks the relays. With Nostr on, a send or establishSecureSession to a peer the device holds no key package for asks the application’s relays for the key packages that peer published, at most once per peer every five minutes while a message waits. Peers on a live Bluetooth or Wi-Fi link and blocked peers are never looked up. Nostr is off by default, so only an application that turned it on sends the new queries; set transports.nostr.coldContactEnabled (nostr_cold_contact_enabled in the interface definition) to false to keep the old behaviour.
  • message_decryption_failed names the decrypt failure. Its reason carries the MLS library’s failure beside the kind, for example (InvalidCiphertext, aead) where it read (InvalidCiphertext). Match on a prefix, not the exact string.
  • Groups: a frame that can never decrypt is acknowledged and dropped. A group frame from a member further behind the receiver’s ratchet than the out-of-order window is acknowledged and dropped, so the sender reports it delivered and stops resending it. A frame from a device outside the group is still held unanswered.
  • Bluetooth LE signal strength is off until the app listens. peer_rssi events flow only while React Native has a listener for that event type (an 'all' listener does not count), or while a native app calls set_peer_rssi_events(true). While they flow, each link’s quality follows the smoothed readings and the mesh relay ranks neighbours by it, so the order in which neighbours carry relayed traffic can change. transports.ble.advertiseNodeHash, which lets an iPhone name an Android peer, is Android only and off by default; read the configuration notes before turning it on.
  • iOS names a central and reports a departed one. The iPhone peripheral serves the Hello characteristic, so a central that writes its identity is bound to its address at once, and the iPhone waits two seconds for a hello before it dials a new central back. A peer whose last link was a central that left is reported lost after 15 seconds, as on Android.
  • Flutter on Android: start() runs a foreground service. The plugin runs the mesh foreground service React Native runs and merges FOREGROUND_SERVICE, FOREGROUND_SERVICE_CONNECTED_DEVICE, CHANGE_WIFI_STATE and a connectedDevice service into the app’s manifest. An app that targets Android 14 or later declares the foreground service type in the Play Console. Reticulum and Nostr configurations, which the plugin used to refuse, now start those transports.
  • Telemetry: ephemeral sends are not counted. Only sends that ask for an acknowledgement count toward the dashboard’s sent and delivered figures.

What v0.31.0 adds

The v0.31.0 changelog has every change.

Upgrade from v0.29

Update to v0.30.0 if your app calls enableTelemetry on 0.26.0 to 0.29.0. In those releases the telemetry uploader trusted the whole bundled Mozilla root set, which still included a root withdrawn after the October 2026 eMudhra intermediate key compromise. Anyone holding one of those keys on the network path could have presented a certificate for the telemetry endpoint and read the telemetry API key and the batches. From v0.30.0 the uploader trusts only the sixteen roots of the four authorities that issue the endpoint’s certificates. The endpoint and proxy handling are unchanged, and no code change is needed. Apps that never call enableTelemetry upload nothing and are not affected. The v0.30.0 changelog and the telemetry transport have the details. The API compiles unchanged. The toolchain is what can stop a build. The SDK’s UPGRADING §28 lists every change that compiles unchanged and behaves differently. Also check these run-time changes:
  • At most 16 direct messages to one nearby peer wait for acknowledgement at once over Bluetooth LE or Wi-Fi Direct. The 17th onward still return an ID, emit message_deferred with reason peer_window instead of message_sent, and go out in order as acknowledgements return. Show a peer_window deferral as queued, not failed. Messages through the relay are not windowed.
  • When 500 messages are already queued, a send to the recipient with the largest backlog returns an ID that then gets message_failed with reason “Outbox capacity exceeded” and no attempts. Treat it as “queue full, try later” for that conversation. Messages already queued are no longer failed to make room.
  • message_delivered reports latency_ms from the first send a carrier accepted, not the last attempt, so latency rises for messages that needed retries.
  • In React Native, message_delivered and message_failed are held while JavaScript cannot take them and arrive on the next subscription or foreground. Keep their handlers idempotent on message_id. A skipped event is now fetched back from the native module and reported as a diagnostic with message: "native_event_replayed". A custom native module that replaces the SDK’s must add replayEvents(fromSeq, toSeq).
  • Telemetry counts only messages sent through the application’s send path as delivered or failed. Expect lower delivered counts in analytics from v0.30.0 devices.
  • The iOS and Android libraries are built for size. They are smaller, and CPU-bound work in the SDK is slower: on the SDK’s benches MLS sealing and opening take about 40% longer and most other paths up to a third longer, a fraction of a millisecond per message.
  • On iOS, an error the bridge does not map to a code reaches JavaScript as the error’s own case and message instead of a generic Foundation description.

What v0.30.0 adds

The v0.30.0 changelog has every change with its reasoning.

Upgrade from v0.28

One change can stop a build. The SDK’s UPGRADING §27 lists every change that compiles unchanged and behaves differently. Also check these run-time changes:
  • Service request and response bodies are sealed in the 1:1 MLS session toward a peer on 0.29.0 once the session is confirmed. A sealed body is about 1.34 times larger on the wire: bodies above about 52 KB no longer cross Bluetooth LE at the 185-byte MTU floor, and above about 47 KiB no longer fit a Nostr event. Turn sealing off with encryption.encryptServiceMessages: false.
  • A service send can now fail where it used to succeed: when sealing fails on a confirmed session, sendServiceRequest and respondToServiceRequest return the error instead of sending plaintext. A peer that has sent a sealed service frame stays sealed.
  • On Android, wifiDirect: { enabled: true } also advertises the phone’s address on the Wi-Fi network it is on and opens streams to iPhones, Python hosts and Android phones there. There is no switch for the LAN alone. The module adds ACCESS_NETWORK_STATE and CHANGE_WIFI_MULTICAST_STATE, granted at install. An app that targets Android 17 (API 37) must declare ACCESS_LOCAL_NETWORK and request it before start(), or the LAN carrier stays off with a warning diagnostic.
  • Without NEARBY_WIFI_DEVICES (or fine location on Android 12 and lower), start() runs the slot on the Wi-Fi network only; Wi-Fi Direct starts after enableTransport('wifiDirect') once granted.
  • Of two Android streams for one address, the one the lower address opened is kept. Inside a Wi-Fi Direct group a client whose address is higher than the owner’s reconnects past a half-open stream after keepalive ends it, about 30 seconds on Android 10 and later.
  • A session now forms between two devices that have never been in range when a third is between them, a resend is offered to the mesh when no carrier reaches the recipient, and a direct message the mesh carried settles with message_delivered. Review any UI that treated a long-unsettled message to a distant peer as lost. A device with allowRelay on also carries these handshake frames.
  • A frame that can never decrypt, arriving over the relay from a peer that advertises ack_versions, is answered with an ACK whose ack_status is undecryptable. The sender re-seals and resends once, and never reports it delivered.
  • Frames the SDK sends on its own carry no_alert, so the relay no longer pushes “new message” to an offline peer for them.
  • message_decryption_failed gains sealed_at_ms and session_established_at_ms.
  • offline-protocol-service starts every transport its configuration enables, and refuses a flag the configuration cannot honour (--listen, --peer or --lan without wifi_direct_enabled, --relay without internet_enabled or with a URL that is not ws:// or wss://).

What v0.29.0 adds

The v0.29.0 changelog has every change with its reasoning.

Upgrade from v0.27

Two changes can stop a build and one changes behaviour at run time. The SDK’s UPGRADING §26 lists every change that compiles unchanged and behaves differently. Also check these run-time changes:
  • wifiDirect: { enabled: true } now starts the transport from start() on both platforms. Before, the setting did nothing without enableTransport('wifiDirect'). On iOS the first start shows the Local Network prompt. Remove the setting if you did not mean to run the transport.
  • On Android 10 and later, wifiDirect.autoAccept: true now forms the Wi-Fi Direct group without a system dialog. An app that already set it starts forming groups on upgrade; set it to false to keep the old behaviour. groupOwnerIntent is deprecated and unused.
  • The Android module declares the Wi-Fi Direct permissions. Request NEARBY_WIFI_DEVICES at run time on Android 13 and later, and ACCESS_FINE_LOCATION on 12 and lower.
  • neighbor_lost fires once, when the last Bluetooth LE or Wi-Fi Direct link to a peer ends. On Android it fires 15 seconds after the last link closes.
  • transport_switched to Wi-Fi Direct fires when the first link proves a peer, not when the transport starts.
  • React Native emits a diagnostic event with message: "native_event_gap" when an event from the native module never reached JavaScript.
  • DiagnosticEvent.level now declares debug. Android emits warning where four GATT server diagnostics used to say warn.
  • Deduplication defaults grow to 5,000 entries retained for 7 days. A config that sets the old values keeps them.
  • DataStore.setInterest called before start() is held and applied during start(). A refused pattern now rejects start(). Declare interests before start() on every launch.
  • Documents are stored and sent only when flush() or flushAll() runs. Flush on a short throttle while the user edits, and once more when editing stops.

What v0.28.0 adds

The v0.28.0 changelog has every change with its reasoning.

Upgrading to 0.21.0

A native rebuild is required, because new source files are compiled in on both platforms. Run pod install for iOS. A JS-only update will not pick them up.
Downgrading is not a rollback. The first launch on 0.21.0 moves delivery state out of the credential store into the app container and deletes the old copy. An older build comes up with an empty outbox, an empty pending queue, and an empty block list, meaning every previously blocked peer is silently unblocked. Roll forward with a hotfix rather than reverting the binary.