Upgrade procedure
- Record the currently deployed package and native-library versions.
- Preserve a supported backup of identity and application state.
- Build against the target release and resolve changed APIs.
- Test restart, pending work and mixed-version peer behaviour on representative devices.
- Release to a limited cohort and inspect failures before expanding.
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 ofpeerBurstandpeerRatePerSec, 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_discoveredand the peer list. - Browser: paced Bluetooth writes. The worker keeps at most four unwritten fragments per link with the page, a sink’s
writemay return a promise, and a write pending for ten seconds closes its link. A custom sink that takes longer passeswriteTimeoutMstoBluetoothCarrier. - React Native: development warnings. With
__DEV__set, the SDK warns when a peer id is not anoff1…address and when calls come in the wrong order. No call is blocked and a release build logs nothing. A test suite that fails onconsole.warnsetsdevPeerAddressWarnings: falseanddevLifecycleWarnings: false, or fixes what they name. See development warnings. ProtocolEngineHosthosts: warning diagnostics. The Flutter plugin in a debuggable Android build, and an iOS host built withDEBUG, receive the ordering hints throughonDiagnosticat levelwarning. On Android the constructor addsintegratorLifecycleWarnings, off by default, beforeonDiagnostic: passonDiagnosticby 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 = falseturns them off. Under the defaultrestrictedpolicy a peer’s request raisesservice_subscribe_receivedand waits foracceptServiceSubscriptionorrejectServiceSubscription; 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, sosendMessageand every other public send fail withInvalidArgumenton 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_discoveredin 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,
sendReadReceiptsends instead of throwing. On iOS,acceptConnectionRequestandsendConnectionRequestaccept an omittedkeyPackage, andcreateInvite(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_welcomereturns nothing for a 1:1 session afterdelete_session. An event callback may callset_event_callback, and a callback that throws no longer costs the polling queue an event. On Android, the native module’sstart()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_diagwith stagesession_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
establishSecureSessionto 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; settransports.nostr.coldContactEnabled(nostr_cold_contact_enabledin the interface definition) to false to keep the old behaviour. message_decryption_failednames the decrypt failure. Itsreasoncarries 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_rssievents flow only while React Native has a listener for that event type (an'all'listener does not count), or while a native app callsset_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 mergesFOREGROUND_SERVICE,FOREGROUND_SERVICE_CONNECTED_DEVICE,CHANGE_WIFI_STATEand aconnectedDeviceservice 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 callsenableTelemetry 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_deferredwith reasonpeer_windowinstead ofmessage_sent, and go out in order as acknowledgements return. Show apeer_windowdeferral 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_failedwith 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_deliveredreportslatency_msfrom the first send a carrier accepted, not the last attempt, so latency rises for messages that needed retries.- In React Native,
message_deliveredandmessage_failedare held while JavaScript cannot take them and arrive on the next subscription or foreground. Keep their handlers idempotent onmessage_id. A skipped event is now fetched back from the native module and reported as adiagnosticwithmessage: "native_event_replayed". A custom native module that replaces the SDK’s must addreplayEvents(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,
sendServiceRequestandrespondToServiceRequestreturn 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 addsACCESS_NETWORK_STATEandCHANGE_WIFI_MULTICAST_STATE, granted at install. An app that targets Android 17 (API 37) must declareACCESS_LOCAL_NETWORKand request it beforestart(), 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 afterenableTransport('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 withallowRelayon 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 whoseack_statusisundecryptable. 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_failedgainssealed_at_msandsession_established_at_ms.offline-protocol-servicestarts every transport its configuration enables, and refuses a flag the configuration cannot honour (--listen,--peeror--lanwithoutwifi_direct_enabled,--relaywithoutinternet_enabledor with a URL that is notws://orwss://).
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 fromstart()on both platforms. Before, the setting did nothing withoutenableTransport('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: truenow forms the Wi-Fi Direct group without a system dialog. An app that already set it starts forming groups on upgrade; set it tofalseto keep the old behaviour.groupOwnerIntentis deprecated and unused. - The Android module declares the Wi-Fi Direct permissions. Request
NEARBY_WIFI_DEVICESat run time on Android 13 and later, andACCESS_FINE_LOCATIONon 12 and lower. neighbor_lostfires 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_switchedto Wi-Fi Direct fires when the first link proves a peer, not when the transport starts.- React Native emits a
diagnosticevent withmessage: "native_event_gap"when an event from the native module never reached JavaScript. DiagnosticEvent.levelnow declaresdebug. Android emitswarningwhere four GATT server diagnostics used to saywarn.- Deduplication defaults grow to 5,000 entries retained for 7 days. A config that sets the old values keeps them.
DataStore.setInterestcalled beforestart()is held and applied duringstart(). A refused pattern now rejectsstart(). Declare interests beforestart()on every launch.- Documents are stored and sent only when
flush()orflushAll()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. Runpod install for iOS. A JS-only update will not pick them up.

