Overview
This guide covers implementing custom backends for storage, network transport, and HTTP operations in whatsapp-rust. The library uses trait-based abstractions to allow full customization.Storage backend architecture
The storage backend is split into four domain-grouped traits:- SignalStore - Signal protocol cryptography (identities, sessions, keys)
- AppSyncStore - WhatsApp app state synchronization
- ProtocolStore - WhatsApp Web protocol alignment (SKDM, LID mapping, device registry)
- DeviceStore - Device persistence operations
Backend Trait
Any type implementing all four traits automatically implementsBackend:
Implementing a custom storage backend
Step 1: define your store
Step 2: implement SignalStore
Handle Signal protocol cryptographic operations:Step 3: implement AppSyncStore
Handle WhatsApp app state synchronization:Step 4: implement ProtocolStore
Handle WhatsApp Web protocol alignment:Pending Inbound Buffer
If you want to support the opt-inInboundDurabilityHook, your backend must implement four additional ProtocolStore methods. Without them, Bot::build() rejects any attempt to register a durability hook with a BotBuilderError::UnsupportedDurabilityBackend error.
Two further methods, The bundled
store_pending_inbound_batch and delete_pending_inbound_batch, back the client’s inbound commit batching during the offline drain. Both default to looping the single-row methods below, so implementing just the four methods in this section is enough — your backend keeps working, just without batch atomicity. Override them if you want the whole batch committed in one transaction:SqliteStore overrides both this way, so the offline-drain batcher pays one Diesel transaction per batch instead of one round-trip per message.store_pending_inbound/get_pending_inbound/delete_pending_inbound/delete_expired_pending_inbound and their batch counterparts do not take device_id as a parameter — (chat, sender, id) is the whole key on the trait. A store that serves a single account scopes rows by (chat, sender, id) alone. If your backend is a shared multi-account store (one table/connection serving several SqliteStore-like instances), scope rows by your own instance’s device id internally, the same way the bundled SqliteStore reads its own self.device_id field rather than accepting it as an argument.The
device_id column exists so one shared table can serve multiple accounts/devices without cross-account collisions — the bundled SqliteStore has one such table per account and always filters by its own self.device_id. A backend with one table per account can drop the column and the device_id predicate entirely.The
message column stores the proto-serialized wa::Message bytes. The client writes them before the Signal ratchet advances so that a crash between the buffer write and the hook return can replay the exact same decrypted bytes on redelivery.Step 5: implement DeviceStore
Handle device data persistence:Using your custom backend
Custom Runtime
TheRuntime trait is the foundation of the runtime-agnostic architecture. It abstracts async task spawning, sleeping, blocking operations, and cooperative yielding. Implement it to use a different async runtime (e.g., async-std, smol, or a WASM executor).
The Runtime trait
AbortHandle
AbortHandle is a type-erased cancellation handle marked #[must_use]. Dropping an AbortHandle aborts the spawned task, ensuring cleanup. Call .detach() to prevent automatic cancellation for fire-and-forget tasks where the task should run to completion even if the parent scope is dropped.
Using your runtime
Spawnable trait (v0.6). The future-bound that generic spawn helpers use is now the Spawnable marker trait instead of a hardcoded Send + 'static. On native targets Spawnable = Send + 'static; on wasm32 it’s just 'static (WASM runtimes are single-threaded and can’t require Send). Runtime::spawn’s signature is unchanged — this only matters if you write your own generic helpers over spawnable futures, in which case bound them on wacore::runtime::Spawnable rather than Send + 'static so they compile on both targets.If you are implementing a single-threaded runtime, override
yield_frequency() to return 1 and have yield_now() return Some(future). This ensures the event loop is not starved during tight processing loops (e.g., decoding incoming frames).Proxy and custom TLS
whatsapp-rust has two separate network layers, each with its own extension point for proxy and TLS customization:
To route all traffic through a proxy, you need to configure both layers.
Custom TLS for the WebSocket transport
UseTokioWebSocketTransportFactory::with_connector to supply a custom TLS Connector — for example, adding custom CA certificates or client certificates:
default_tls_connector() to inspect or replicate the default TLS configuration as a starting point.
Full proxy support for the WebSocket transport
For full proxy support (SOCKS5, HTTP CONNECT, etc.), implement theTransportFactory trait directly. Establish the WebSocket connection through your proxy, then wrap it with from_websocket:
Proxy support for the HTTP client
UseUreqHttpClient::with_agent to supply a pre-configured ureq::Agent with proxy settings:
HttpClient trait directly for full control over HTTP behavior.
Combining both layers
To route all traffic through a proxy, configure both the transport factory and the HTTP client:The WebSocket transport handles the persistent WhatsApp protocol connection, while the HTTP client handles media operations (upload/download) and version fetching. Both must be configured separately for full proxy coverage.
Custom transport backend
Transport Trait
Implement theTransport trait for custom network transports:
Transport Factory
ImplementTransportFactory to create transport instances:
Transport Events
Custom HTTP Client
Implement theHttpClient trait for custom HTTP operations:
If your HTTP client doesn’t support streaming, you only need to implement
execute. The supports_streaming method defaults to false, and download_to_writer will automatically use a buffered fallback.SQLite reference implementation
The library includes a full SQLite implementation you can use as reference: See Store API reference for the SQLite implementation details.Key Features
- Diesel ORM with migrations
- Connection pooling (r2d2)
- WAL mode for concurrency
- Prepared statements for performance
- Transaction support
- Database snapshotting for debugging
Using SQLite store
SQLite is bundled by default via the
bundled-sqlite feature on whatsapp-rust-sqlite-storage. No system SQLite installation is required.Custom cache store
The pluggable cache store adapter lets you replace the default in-process caches with an external backend like Redis or Memcached. This is useful for sharing cache state across multiple client instances or for deployments where in-process memory is limited.The CacheStore trait
Implement theCacheStore trait from wacore::store::cache:
Plugging in your cache store
UseCacheStores to assign your custom backend to specific caches:
Available namespaces
The following namespaces are used internally by the client:Design considerations
- Error handling is best-effort. Cache misses and failures are logged as warnings but don’t break the client — it falls back to fetching from the authoritative source.
- Serialization uses
serde_json. Values are serialized to JSON bytes on the custom-store path. The in-process path has zero serialization overhead. - TTL is forwarded from
CacheEntryConfig. Your implementation receives the same TTL configured inCacheConfig. - Coordination caches cannot be externalized. Session locks, message queues, and enqueue locks hold live Rust objects (mutexes, channels) and always stay in-process.
invalidate_all()requirestokio-runtime. The synchronousinvalidate_all()method onTypedCachespawns a fire-and-forget task via Tokio for custom backends. Without thetokio-runtimefeature, the clear is skipped with a warning. Use the asyncclear()method instead if you disabletokio-runtime.
Best Practices
Next Steps
- Sending Messages - Use your custom backend
- Receiving Messages - Store received messages
- Group Management - Store group metadata
- Inbound Durability Hook - At-least-once delivery with your backend