Skip to content

v0.7.0

Latest

Choose a tag to compare

@github-actions github-actions released this 07 Aug 03:09
· 40 commits to main since this release
f8165f2

More than a year in

The repo went up on 1 July 2025. Today: 1,462 commits, 17 contributors, around 700 stars, and a client that pairs by QR or phone number, encrypts one-to-one and group messages, syncs history, moves media, and places and answers real WhatsApp calls with audio and video.

wacore was built with no runtime of its own, so the same protocol code runs anywhere. Three projects took it up on that:

  • whatsapp-rust-bridge compiles the core to wasm for JavaScript. It lives under WhiskeySockets, and baileys@latest (the 7.0 line, in release candidate) depends on it directly. Baileys is the reference WhatsApp Web library for JavaScript, at roughly a million installs a week.
  • baileyrs: a WhatsApp Web library for JavaScript, powered by Rust, with a Baileys-compatible API.
  • whatsapp-rust-esp32: a full end-to-end-encrypted WhatsApp client on an ESP32-S3. No Tokio, hundreds of kilobytes of RAM, and it pairs and exchanges encrypted messages. Every "this has to stay runtime-agnostic" review comment was paying for that one.

None of it was planned in July 2025. Thank you to everyone who filed an issue with a stanza dump attached, argued with a diff, or shipped something on top of this and told us what broke. A reverse-engineered protocol is not something one person figures out.

This release

488 pull requests since 0.6.0, over roughly three months. The big items are VoIP (1:1 voice, 1:1 video, group calls and call links, with the media plane and not only the signaling), the switch of protobuf codegen from prost to buffa, a pre-1.0 pass over the public API (typed errors, sealed event payloads, #[non_exhaustive]), a plugin host, an optional SQLite chat store, opt-in tracing and metrics, and a per-message round trip that allocates about a third less than 0.6.0's.

A lot of APIs changed, and the Breaking changes section below is the one to read before upgrading. The two invasive ones are buffa's generated proto surface and the anyhow to typed-error migration, and both touch nearly every call site in a downstream crate. They are taken now because breaking changes are still cheap before 1.0.

Highlights

VoIP

  • 1:1 voice calls (#918). Incoming and outgoing, against the official WhatsApp app, with the full media path: mic capture, encode, encrypt, relay, decrypt, decode, playout. Includes a pure-Rust MLow codec (WhatsApp's proprietary voice codec, ported from the reverse-engineered reference and pinned by a byte-exact golden roundtrip test), E2E-SRTP with the WARP integrity tag and the SFrame layer, and a sans-IO CallEngine that owns no socket, clock or thread. No codec FFI, so it still builds for wasm and embedded targets.
  • 1:1 video calls (#1024). Start with video, upgrade from audio, downgrade back, and accept the same transitions from an official client. Codec-neutral: consumers pass complete H.264 Annex-B access units through VideoSource / VideoSink. Authenticated PLI/FIR is treated as a decoder resynchronization boundary, which is what made real webcam video render on the Android peer.
  • Group calls and call links (#1130). Group-bound calls, ad-hoc promotion of an active direct call, reusable call links, 128-user waiting rooms with admission and administrator controls, roster-driven subscriptions, atomic epoch rotation and key fan-out, participant-attributed audio, video, RTCP and data, plus reactions, hand raising and screen sharing.
  • Encoded audio and native Opus (#1050). Applications can supply raw MLow or Opus packets without linking a codec. Full-duplex native Opus negotiation (PT120/16 kHz as the production profile, PT111/48 kHz as an explicit variant). MLow stays the compatibility default.
  • tctoken on outgoing call offers (#970), zero-copy RTP/STUN parsing (#957), and a tail of live-tested relay and signaling fixes (#1102, #1104, #1107, #1128).
let handle = client.voip()
    .call(peer)
    .audio(mic_source, speaker_sink)
    .video(h264_source, h264_sink)   // optional
    .start()
    .await?;

handle.wait_ended().await;

The media plane sits behind optional features (voip, voip-runtime, voip-mlow, voip-libopus, voip-encoded), and a default build pulls none of it. Call signaling is not gated: the IncomingCall and MissedCall events, and reject and terminate, are there in a default build.

Extensibility

  • Plugin architecture (#1061). A build-time native plugin host behind the plugins feature, with a generation-scoped extension lifecycle (client-lifecycle), typed plugin APIs, capabilities, dependency ordering, scoped tasks with completion barriers, and a bounded event router keyed by (plugin_id, topic).
  • whatsapp-rust-chat-store (#1014). An opt-in crate that materializes the client's event stream into queryable SQLite tables, in the same database file as the device store, so a UI or a stateful bot survives a restart without re-syncing. Event-sourced write-behind writer, encoded wa::Message as the source of truth, keyset pagination, a StoreChange broadcast for invalidation, and FTS5 search. Not on crates.io yet: the schema and the query surface are still moving, so it is consumed by git path for now.
  • Optional tracing (#733): around 172 spans under a wa.{conn,recv,send,iq,appstate,pair,media,receipt,retry,pdo,notif,session,bot}.* taxonomy, OTel-ready, with phone numbers redacted by default. Optional metrics (#734) through the metrics facade, plus a whatsapp-rust-plugin-metrics plugin (also unpublished, by git path). Both are off by default, with no dependency and no overhead when disabled.
  • Per-session resource attribution in stats() (#967).
  • Typed legacy session interop (#1072) behind legacy-session-interop, for importing externally produced auth state.
  • A SQLite connection-init hook that runs before pragmas and migrations (#1074).

Delivery guarantees

  • Opt-in inbound durability hook (#920) turns the inbound consumer from at-most-once into at-least-once. The transport ack is deferred until the hook commits, and the decrypted message is buffered durably before the Signal ratchet flushes. With no hook registered the behaviour is unchanged.
  • Ordered and bounded event delivery (#981). EventDelivery::Ordered { capacity } delivers callbacks in arrival order through one bounded mailbox, matching WA Web's preserveOrder: true. A full mailbox drops the event and counts it in StatsSnapshot::events_dropped instead of growing without limit.
  • Batched inbound commit during the offline drain (#961). WA Web commits durability, signal-store writes and aggregate receipts once per snapshot; we were doing all of it per message. A 421-message backlog was spending about 1.2 s in per-message round-trips.
  • An opt-in RetryAdmission hook (#985) and an observe-only ServerAck event (#989).

Messaging surface

  • Passkey companion linking (#928): WhatsApp's SHORTCAKE_PASSKEY WebAuthn device-link gate, end to end, with a PasskeyAuthenticator seam for the one step that cannot be reproduced in-tree.
  • Reactions (#720), message forwarding (#738), keep-in-chat (#740), quiz polls (#754), events with RSVP (#758), chat labels (#715), a 1:1 disappearing-message timer (#745), mark_as_played for voice and video notes (#737), group profile pictures (#739), save_contact (#742), the clearChat (#755) and userStatusMute (#760) app-state actions, and newsletter mute/unmute (#757).
  • Encrypted channel (CAG) reactions and comments, both directions (#830).
  • Meta AI and fbid bot replies via <enc type="msmsg"> (#650).
  • Secret-encrypted envelopes: poll-edit, poll-add-option and event-edit (#645), and message edits on receive (#665, #667, #762).
  • Constant-memory streaming upload with a media sidecar (#684), high-level media builders from UploadResponse (#764), ContextInfo in media options (#931), and media download without a connected session (#1194).
  • First-party sticker packs from the CDN (#644), and the verified business name on messages and usync (#741, #1081).

Generated protocol registries

Hand-maintained protocol tables were replaced with registries generated from the whatspec IR, so refreshing them is a file copy instead of an audit.

  • mex operations (#728), with typed Variables and Response per op. This also fixed two doc-ids the current WA Web bundle no longer ships, which had been failing silently.
  • A/B props (#729): 2100 typed flags replacing 11 magic u32s, at no binary cost for the ones nothing references.
  • App-state (syncd) schemas (#716). Every action now carries its own version, scope and index shape; we had been stamping version = 1 on all of them.
  • Proto bumped to WhatsApp Web 2.3000.1040878135 (#726), then 2.3000.1042742319 (#1001).

Performance

The allocation work from 0.6.0 continued, with one change in method: CodSpeed now runs deterministic instruction-count and memory instruments on every PR (#828, #898, #950), so a regression is caught before merge and every number below was measured rather than estimated.

Per-message round trip

The ping-pong allocation series (#1015 to #1020) took 32.3% off the bytes allocated per cycle, from a 46,088 B baseline, across six PRs: hot-path allocations, a persistent receipt worker, sync fast paths for the store adapter, exact stanza sizing, per-branch boxed send futures and an inlined marshal hint cache.

A second sweep removed another dozen-plus allocations from the DM round trip (#1114, #1116, #1122, #1131, #1137), coalesced queued noise frames into one transport write (#1119, #1120, #1121) and memoized the DM device fan-out per recipient (#1118).

Signal and crypto

  • Counter leasing (#1026, #1048): warm group encryption went from 84.69 ms to 23.98 ms (-71.7%), warm participant fan-out from 49.16 ms to 8.69 ms (-82.3%).
  • The sender-key backlog moved behind an Arc (#881), taking an in-order decrypt on top of a ~2000-key backlog from a 97 µs to a 33 µs median and removing the ~82 KB allocation every decrypt was paying. It is now stored as Copy pairs as well (#953), which takes the worst-case out-of-order group decrypt from 163.7 µs to 113.3 µs (-31%).
  • XEdDSA memoization for sender signing and verification (#838, #839, #1213), pre-keyed zero-salt HKDF extracts (#847, #863, #864), and shift-free eviction of skipped message keys (#946).

Binary protocol and JID

  • Token lookup moved to a length-bucketed tiny_map on encode (#873), taking marshal_auto_small from 168.7 ns to 111.8 ns (-34%), and then to a build-time static hash table (#1056) that is 29 KiB smaller, faster on hits, and drops the hashify dependency. marshal_exact got 3x faster by replaying plan-pass hints instead of reclassifying every string (#1057).
  • Small attribute lists are stored inline (#819): a message-shaped stanza went from 15 to 11 allocations, an 800-participant group stanza from 4012 to 2412 (-40%). Packed values are unpacked through a byte-pair table (#1214), and the per-node box around NodeRef content is gone (#1216), worth -9.5% instructions on the fan-out bench and -40% on the allocation count.
  • Jid display emits one write_str (#848), 176.7 ns to 69.7 ns, followed by a parsing sweep that stops the scan at the @, reads the device and agent fields with a decimal-only reader, and stops re-validating UTF-8 the writer just assembled (#1171 to #1173, #1176, #1183, #1187), with the same pass trimming node child iteration and attribute lookups (#1174, #1175).
  • Wire strings are validated through smoothutf8 (#932), and inflate writes into an uninitialized buffer, dropping a memset that was 6% self time (#933).

App-state and history sync

  • ltHash index-MAC dedup: bench_collect_unique_index_macs[1000] went from 5,678 µs to 550.8 µs (#868), then another -19% at 1000 and -72% at 10 with inline index-MAC keys (#955). The N+1 previous-MAC lookups became a single query (#687, #821), and the O(n²) in-patch overwrite scan became an O(1) map (#701).
  • History sync got streaming decompression (#672), a pooled inflate reader (#683), storage of the compressed payload with a streaming reader (#853) and a decode-gating scan (#836). Together: allocation churn from 20.20 MB to 14.43 MB (-28.6%), CodSpeed stream-drain memory from 243.8 KB to 115.2 KB, and -16.2% peak heap on a 20k-row secret batch (#1058, #1054).

Send path and caching

  • The per-group device fan-out is memoized behind a topology generation (#824), the group phash is memoized on the same entry (#840), warm sends stay warm under the own-device SKDM steady state (#1021), and cold sender-key distribution is single-flight (#937).
  • One message encode is shared between the reporting token and the DM or group plaintext (#904, #905), and DM content is encoded once and spliced into the recipient and DSM plaintexts (#787, #788).
  • Device-registry lookups borrow their keys and build them inline (#681, #682), taking per-member device resolution on an 800-member group send from 50,915 to 12,538 allocations (-75%).
  • moka is gone. PortableCache is the only in-process cache backend (#860).

Storage

  • Per-session memory and thread use was cut and made configurable (#926), and the per-checkout SELECT 1 liveness probe (36.6% of the bare checkout cost) was removed (#874).
  • WAL readers are no longer blocked by search (#1151): an FTS5 prefix query over 60k rows went from 13.3 ms to 0.8 ms.

Binary size

Binary size is now a CI gate with a per-PR budget and a historical series (#859).

  • waproto::codec was pinned to a single instantiation (#842), taking .text from 13.03 to 11.85 MiB (-9.1%). Cold entry-point futures were boxed (#843) and the proto PartialEq anchor dropped, taking 148 eq impls to 0 (#846).
  • wa::Message shrank from 3784 to 952 bytes (-75%) by boxing its inline content variants (#866).
  • A full audit landed -1.47 MiB stripped (-13.3%) and -14.8% .text (#1055) through -Zshare-generics, lld with ICF, SQLite at -Oz with a subsystem trim, and the rest of the codec sweep. Every rejected lever is documented with its drawback.

WhatsApp Web compliance and reliability

  • App-state convergence (#1158). A snapshotMac mismatch on a patch means the collection's ltHash has diverged, not that the server is tampering. We treated it as fatal and re-requested the same unusable patch forever (the report was 114 identical failures over one evening, surviving restarts). WA Web degrades the collection instead. The same PR stops losing patches rejected with 409. Related: aggregate MAC validation for genesis patches (#988), snapshot-rollback and duplicate-index guards (#752, #753), index-mode ltHash for SET plus REMOVE on the same index (#829), and key recovery across companion devices (#1052).
  • Signal durability (#1027, #1041 to #1044): sender-key advances gated before the wire, retry advances persisted before the wire, durability gates retained through deletes, sender-key mutations serialized, and cancelled session checkouts recovered. A deterministic chaos suite covers it (#1049).
  • A blind WA Web parity sweep (#965) raised the offline-drain snapshot from 200 to 400, lowered the Signal forward-jump cap from 25000 to 2000, made the reconnect backoff reset only after a stable connection of 30 s or more (with explicit penalties surviving the reset), and made <xml-not-well-formed> force-close the socket.
  • Top-level <status> stanzas are handled and acked (#1101). An unacked one was recycling the stream.
  • Pair-code lifetime (#1163). A phone-number link no longer dies with the QR rotation. The stage-1 and stage-2 crypto was re-audited and cleared first. The companion_finish answer is now read instead of assumed (#1198), and link_code_pairing_nonce was corrected (#976, #979).
  • JID correctness (#1178, #1182, #1184). The AD-JID domain byte was being kept as an agent, so encode followed by decode was not idempotent. Agent identity, the AD form and device dedup now agree, and interop JIDs encode with the token that carries their integrator.
  • LID and PN: DM wire addressing is gated on the account's 1:1 migration state (#943), the write policy is source-aware and matches createLidPnMappings (#1011), and mappings are learned from phoneNumberToLidMappings (#1010) and from embedder-supplied sources (#1013).
  • Retry and SKDM: recoverable group and 1:1 decrypt failures retry instead of NACK (#986, #987), the per-group resend rate is bounded to avoid AccountLocked (#871), SKDM redistribution is gated on the primary device (#872), own devices are never memoized in the sender-key map (#999), per-device sessions are locked across the SKDM fan-out (#990), and the inbound sender-key chain is serialized (#992).
  • Hardening: node-decode recursion depth is bounded against hostile frames (#994), nothing is sent after a transport failure so the nonce cannot be reused (#1117), the keepalive watchdog is anchored to first send (#995), noise frame-counter exhaustion is an error rather than a wrap (#782), the WARP MI tag is authenticated before recv ROC state is folded (#998), download_to_writer leaves only verified media (#1197), and self-only protocol messages are honored only from our own account (#646).
  • Connection lifecycle: the failure stanza is no longer dropped on logout, ban or outdated (#1165), the reconnect backoff races the terminal shutdown (#1179), the Origin header is sent on the WebSocket upgrade (#1166), and SIGTERM shuts the client down cleanly (#960).

Breaking changes

Protobuf codegen: prost to buffa

#557 migrates codegen to buffa and puts its zero-copy views on the real decode paths. This changes the shape of every generated type:

Pattern Before (prost) After (buffa)
Sub-message fields Option<T>, Option<Box<T>> where recursive MessageField<T> (.as_option() / .is_set())
Enum fields Option<i32> Option<EnumType> (typed)
Enum variants CamelCase SCREAMING_SNAKE_CASE, with CamelCase aliases still generated
Type names AdvSignedDeviceIdentity ADVSignedDeviceIdentity
Decode (owned) Type::decode(bytes) Type::decode_from_slice(bytes)
Decode (borrowed) not available TypeView::decode_view(bytes)
Encode msg.encode(&mut buf)? msg.encode_to_vec()

whatsapp.rs is no longer tracked: build.rs generates it into OUT_DIR from the committed descriptor, so consumers never need protoc. Proto field names are snake_case, which is wire-compatible. Encoding a waproto type from outside waproto should go through the pinned waproto::codec helpers (#1091); a clippy.toml guard enforces that in-repo.

Errors

  • anyhow no longer appears in any public signature (#893). There is one typed error per feature domain (SendError, GroupError, ContactError, BlockingError, ChatStateError, PresenceError, CommunityError, NewsletterError, ProfileError, PollError, SignalError, TcTokenError, MediaReuploadError, AppStateError), all built on a shared ClientError base with #[from] chaining. Every listed method's Err variant changed, though ? into an anyhow context still works.
  • ConnectError and ConnectStage (#1090): connect(), wait_for_socket() and wait_for_connected() return Result<(), ConnectError>, ClientError::AlreadyConnected is gone, and logout() is now infallible.
  • All 46 occurrences of #[error(transparent)] became #[error("{0}")] (#1100). The Display output is byte-for-byte identical, but the wrapped typed error stays reachable through source(), so a 403 or a 409 can be classified without matching on strings. ErrorChainExt writes that walk once. A refused HTTP status is recoverable by type (#1195).
  • The read loop exits with a typed reason (#956). Routine server recycles are not errors, and Disconnected carries the reason.

Public API freeze ahead of 1.0

  • Event payloads are sealed (#1000, #1002 to #1004): 25 field-bearing payloads plus the notification and sync families are #[non_exhaustive] with a bon builder, three inline Event variants became sealed newtypes, and four unit markers became empty sealed structs. EventInterest moved from u64 to u128. Payloads are now built with Type::builder().build().
  • #[non_exhaustive] was added to public error enums, public value enums and lib-constructed response structs (#735, #736, #794), and dead privacy enums were dropped.
  • The bot API was overhauled (#852). bot.run().await?.await? becomes bot.run().await in the foreground or bot.spawn() in the background, returning a BotHandle with client(), a graceful shutdown() and abort(). with_backend(arc) becomes with_backend_arc, whatsapp_rust::transport::UreqHttpClient moved to whatsapp_rust::http, bot::Missing was replaced by named typestate markers, and BotBuilderError::Other is gone. Registering two handlers now runs both, where the second used to silently replace the first. There are typed registrars (on_message, on_qr_code, on_pair_code, on_connected, on_logged_out) and a prelude.
  • One dependency line is enough (#852, #954): wacore, wacore_binary and waproto are re-exported wholesale.
  • impl Into<Jid> across the public surface (#834), alloc-aware message-id parameter types (#775), a DownloadParams struct instead of six positional arguments (#768), get_participating returning a HashMap (#767), and tagged WireEnum variants serialized as structs (#1096).

Persistence and state

  • sqlite-storage persists blobs as protobuf instead of bincode (#911), through buffa once #557 landed on top of it.
  • messageSecret retention is bounded by policy and by an event-time horizon (#668).
  • The Signal cache flushes on disconnect, which stops SKDM re-fanout (#670).
  • App-state Mutation no longer stores duplicate index and value MACs (#689), and previous-MAC lookups are batched (#687).
  • FDownload is a &BlobDownloadFn trait object instead of a generic parameter (#1055), which affects wacore consumers calling the app-state sync entry points directly.
  • wa::Message boxes its inline content variants (#866), so pattern matches that bound those fields by value need a deref.
  • Message edits are decrypted on receive (#665), and the inbound commit pipeline batches during the offline drain (#961), which changes when handlers observe drained messages.

Cargo features

  • Removed: moka-cache, which was in the default set, so a build pinning default-features = false and listing it by name has to drop it (moka is gone entirely, PortableCache is the only backend), and debug-diagnostics.
  • Added: voip, voip-runtime, voip-mlow, voip-libopus, voip-encoded, plugins, client-lifecycle, tracing, tracing-pii, metrics, legacy-session-interop, danger-skip-cert-chain-verify.
  • New workspace members: storages/chat-store, plugins/metrics, examples/voip-cli.
  • MSRV is declared for the first time, at 1.94, and enforced in CI (#1093). 0.6.0 declared none.
  • prost is no longer in the dependency graph at all, which shows up in your lockfile and your license audit as well as at the call sites.

Tooling and CI

  • CodSpeed benchmarks run on every PR, with deterministic RNG and hashers, a fixed 2-worker runtime and sharding (#828, #835, #898, #902, #950), plus benchmarks built from the stanza shapes production actually sends (#1186).
  • Binary size has a per-PR budget gate and a historical series (#859).
  • Supply chain, docs, MSRV and feature-matrix gates were added (#1093): workspace lints (dbg_macro and todo denied), cargo-deny, a rustdoc gate and a per-feature build check.
  • Miri covers the unsafe decode path (#1162), the decoder has roundtrip property tests and an unmarshal fuzz target (#895), and fixed sleeps in tests were replaced with bounded polling (#1094).
  • The test suites run through cargo-nextest (#1209), and the Docker image is multi-arch, runs unprivileged and is published to GHCR (#927).

Contributors

Thanks to everyone who shipped code into this release:

  • @jlucaso1: VoIP (voice, video, group calls, the MLow codec), the buffa migration, the error and event API freeze, the plugin host, chat-store, tracing and metrics, and most of the performance, WA Web parity and durability work.
  • @Salientekill: admin-revoke fan-out fix, prekey re-upload after re-pair, groupStatusV2 unwrapping, participant phone-number backfill, edit_message_with_stanza_id.
  • @blaueeiner: PDO placeholder-resend skips for view-once, PN-LID mappings from history sync, public add_lid_pn_mapping, pushname matching, app-state collection reservation.
  • @Bot-Dev-RPA: removal of the obsolete device-registry cleanup task, VoIP relay port racing, sibling-device call handling.
  • @zdanysfa: <ack> recipient preservation and softened unknown stream:error, incoming peer message-edit decryption.
  • @arsa0x: ContextInfo in media options.
  • @JeanCapixaba: the observe-only ServerAck event.
  • @oonid: own-device LID namespace alignment for LID-addressed DMs.
  • @alexandme: Rust 1.93 build fix for the pushname arm.
  • @dependabot: kept dependencies current across the release window.
  • @codspeed-hq: the CodSpeed harness this release's performance numbers are measured on (#828).

New contributors


Full changelog: v0.6.0...v0.7.0, 488 PRs across VoIP, the protobuf migration, the pre-1.0 API work, performance, WA Web parity and reliability.

All PRs in this release (auto-generated, full attribution)