Skip to main content

Overview

whatsapp-rust uses a trait-based storage system to persist device state, cryptographic keys, and protocol metadata. The storage layer is split into five domain-specific traits:
  • SignalStore - Signal protocol cryptographic operations (identity keys, sessions, pre-keys, sender keys)
  • AppSyncStore - WhatsApp app state synchronization (sync keys, versions, mutation MACs)
  • ProtocolStore - WhatsApp protocol alignment (SKDM tracking, LID-PN mapping, device registry)
  • MsgSecretStore - messageSecret persistence for poll/edit/bot-reply decryption (added in v0.6)
  • DeviceStore - Device persistence operations
All five traits are combined into the Backend trait for convenience.

The backend trait

Any type implementing all five domain traits automatically implements Backend:
MsgSecretStore became a required member of Backend in v0.6, so a custom backend must implement it (the bundled SqliteStore already does). Its methods have defaults that keep the surface small — see MsgSecretStore for which methods you actually need to write.

SignalStore Trait

Handles Signal protocol cryptographic storage for end-to-end encryption.

Identity Operations

Session Operations

PreKey Operations

Signed PreKey operations

Sender key operations

For group messaging encryption:

AppSyncStore Trait

Handles WhatsApp app state synchronization storage.

Sync key operations

Version Tracking

Mutation MAC Operations

get_mutation_macs was added in v0.6 to collapse the app-state sync’s per-mutation previous-MAC lookups (which were N+1) into a single batched query. It has a default implementation, so existing custom backends keep working — override it with a WHERE index_mac IN (…) query for the performance win. The SQLite store chunks the IN list at 500 entries.

ProtocolStore Trait

Handles WhatsApp protocol alignment and tracking.

Per-device sender key tracking

Tracks sender key distribution status per device in groups, matching WhatsApp Web’s participant.senderKey Map<deviceJid, boolean> model. Each device has a boolean indicating whether it holds a valid sender key (true) or needs a fresh SKDM (false).

LID-PN Mapping

Manages mappings between LID (Locally Indexed Device) and phone numbers:

Base key collision detection

Device Registry

TcToken Storage

Trusted contact privacy tokens:

Sent message store

Persists sent message payloads for retry handling. Matches WhatsApp Web’s getMessageTable pattern where retry receipts look up the original message from storage.
The take_sent_message method is an atomic read-and-delete operation. Once a message payload is taken for retry, it is removed from storage to prevent double-retry. For status broadcasts where multiple devices may retry, the client re-adds the message after taking it.

MsgSecretStore

The fifth required member of Backend. It persists the 32-byte messageSecret values needed to decrypt later add-ons keyed off an original message: poll votes, poll/event edits, message edits (secret_encrypted_message), and Meta AI / fbid bot replies (<enc type="msmsg">). Secrets are keyed by (chat, sender, msg_id) and carry an absolute expiry so they can be pruned by policy (see messageSecret retention).
MsgSecretEntry is { chat, sender, msg_id, secret, expires_at, message_ts }. The SQLite table is:
A custom backend only needs to implement three methods: put_msg_secrets, get_msg_secret, and delete_expired_msg_secrets. The other two have defaults — put_msg_secret delegates to put_msg_secrets with expires_at = 0, and get_msg_secret_with_ts pairs get_msg_secret with a 0 timestamp. Override get_msg_secret_with_ts only if your store persists message_ts and you want the edit-window enforced.

DeviceStore Trait

Handles device data persistence:

SqliteStore implementation

The default storage implementation using SQLite with Diesel ORM. SQLite is bundled by default — you don’t need it installed on your system.

Bundled SQLite

The whatsapp-rust-sqlite-storage crate enables the bundled-sqlite feature by default, which compiles SQLite from source and statically links it. To use a system-installed SQLite instead:
Cargo.toml

Creating a store

Features

  • Connection pooling - Uses Diesel r2d2 with pool size of 2
  • WAL mode - Write-Ahead Logging for better concurrency
  • Automatic migrations - Runs embedded migrations on startup
  • Semaphore-based locking - Prevents concurrent writes
  • Retry logic - Automatic retry with exponential backoff for locked database
  • Multi-device support - Single database can store multiple device sessions

Database Configuration

SqliteStore automatically configures connections with:

Usage Example

CacheStore Trait

The CacheStore trait enables pluggable cache backends for the client’s data caches. By default, caches use in-process moka; implementing this trait lets you use Redis, Memcached, or any other external cache. Location: wacore/src/store/cache.rs

Namespaces

Each logical cache uses a unique namespace string. Implementations should partition keys by namespace (e.g., prefix as {namespace}:{key} in Redis).

Error handling

Cache operations are best-effort. The client treats read failures as cache misses and logs warnings on write failures. Implementations should still return errors for observability.

CacheStores configuration

Set individual caches or use CacheStores::all(store) to route all pluggable caches to the same backend:
See Custom backends — cache store for a full implementation example.

TypedCache

TypedCache<K, V> is a generic wrapper that dispatches to either moka or a custom CacheStore backend. Location: src/cache_store.rs
invalidate_all() on custom CacheStore backends requires the tokio-runtime feature. Without it, the clear is silently skipped. Use the async clear() method as an alternative.

Implementing custom storage

To implement a custom storage backend:
  1. Implement all four domain traits
  2. The Backend trait is automatically implemented
  3. All methods must be async and thread-safe (Send + Sync)

Example: Redis store

Best Practices

  1. Thread Safety - Use Arc for shared state, Mutex for mutable state
  2. Error Handling - Convert backend errors to StoreError variants
  3. Transactions - Use database transactions for atomic operations
  4. Retries - Implement retry logic for transient failures
  5. Connection Pooling - Reuse connections when possible
  6. Blocking Operations - Wrap blocking I/O in tokio::task::spawn_blocking

Data Structures

AppStateSyncKey

LidPnMappingEntry

TcTokenEntry

DeviceListRecord

The raw_id field stores the ADV (Account Device Verification) key index list raw_id from device notifications. When this value changes for a user, it indicates an identity change (e.g., the user reinstalled WhatsApp). The client uses this to detect identity changes and clear Signal sessions for that user’s non-primary devices. Per-device sender key tracking is not wiped globally on identity change — that would empty the tracker too aggressively and feed the no-distribution path on the next group send. SKDM redistribution is instead driven per-group/per-device by retry receipts (matching WhatsApp Web’s WAWebUpdateLocalSignalSession/markForgetSenderKey behavior).

Error Handling

All storage operations return Result<T> from wacore::store::error. Each variant preserves the underlying typed error as its source() so callers can downcast to the original backend error when needed:
StoreError exposes a helper is_database_busy_or_locked() that walks the source chain looking for SQLite BUSY/LOCKED markers. Retry layers use it to decide whether a database error is transient without depending on a specific backend crate.

See Also