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: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
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:| Layer | Purpose | Extension point |
|---|---|---|
| WebSocket transport | WhatsApp protocol connection | TransportFactory trait / with_connector |
| HTTP client | Media upload/download, version fetching | HttpClient trait / UreqHttpClient::with_agent |
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 moka 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:| Namespace | Cache | Description |
|---|---|---|
"group" | group_cache | Group metadata |
"device_registry" | device_registry_cache | Device registry entries |
"lid_pn_by_lid" | lid_pn_cache | LID-to-phone bidirectional mappings |
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 moka 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
// ✅ Good: Use connection pool
let pool = Pool::new(config)?;
let backend = MyCustomStore::new(pool);
// ❌ Bad: Create connections per operation
let backend = MyCustomStore::new(connection_string);
use wacore::store::error::{Result, StoreError};
async fn load_session(&self, address: &str) -> Result<Option<Vec<u8>>> {
match self.connection.query("SELECT ...", &[address]).await {
Ok(row) => Ok(Some(row.get(0))),
Err(e) if is_not_found(&e) => Ok(None),
Err(e) => Err(StoreError::Database(e.to_string())),
}
}
async fn put_mutation_macs(
&self,
name: &str,
version: u64,
mutations: &[AppStateMutationMAC],
) -> Result<()> {
let tx = self.connection.begin_transaction().await?;
for mutation in mutations {
tx.execute("INSERT ...", &[...]).await?;
}
tx.commit().await?;
Ok(())
}
Next Steps
- Sending Messages - Use your custom backend
- Receiving Messages - Store received messages
- Group Management - Store group metadata