> ## Documentation Index
> Fetch the complete documentation index at: https://whatsapp-rust.jlucaso.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Storage Traits

> Storage backend traits and SQLite implementation for persistent state

## 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`:

```rust theme={null}
pub trait Backend:
    SignalStore + AppSyncStore + ProtocolStore + MsgSecretStore + DeviceStore + Send + Sync {}

impl<T> Backend for T
where
    T: SignalStore + AppSyncStore + ProtocolStore + MsgSecretStore + DeviceStore + Send + Sync
{}
```

<Note>
  `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](#msgsecretstore) for which methods you actually need to write.
</Note>

## SignalStore Trait

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

### Identity Operations

```rust theme={null}
/// Store an identity key for a remote address
async fn put_identity(&self, address: &str, key: [u8; 32]) -> Result<()>;

/// Load an identity key for a remote address
async fn load_identity(&self, address: &str) -> Result<Option<Vec<u8>>>;

/// Delete an identity key
async fn delete_identity(&self, address: &str) -> Result<()>;
```

### Session Operations

```rust theme={null}
/// Get an encrypted session for an address
async fn get_session(&self, address: &str) -> Result<Option<Vec<u8>>>;

/// Store an encrypted session
async fn put_session(&self, address: &str, session: &[u8]) -> Result<()>;

/// Delete a session
async fn delete_session(&self, address: &str) -> Result<()>;

/// Check if a session exists (default implementation uses get_session)
async fn has_session(&self, address: &str) -> Result<bool>;
```

### PreKey Operations

```rust theme={null}
/// Store a pre-key
async fn store_prekey(&self, id: u32, record: &[u8], uploaded: bool) -> Result<()>;

/// Store multiple pre-keys in a single batch operation (default loops over store_prekey)
async fn store_prekeys_batch(&self, keys: &[(u32, Vec<u8>)], uploaded: bool) -> Result<()>;

/// Load a pre-key by ID
async fn load_prekey(&self, id: u32) -> Result<Option<Vec<u8>>>;

/// Load multiple pre-keys by ID in a single batch operation (default loops over load_prekey)
async fn load_prekeys_batch(&self, ids: &[u32]) -> Result<Vec<(u32, Vec<u8>)>>;

/// Remove a pre-key
async fn remove_prekey(&self, id: u32) -> Result<()>;

/// Get the highest pre-key ID currently stored
async fn get_max_prekey_id(&self) -> Result<u32>;
```

### Signed PreKey operations

```rust theme={null}
/// Store a signed pre-key
async fn store_signed_prekey(&self, id: u32, record: &[u8]) -> Result<()>;

/// Load a signed pre-key by ID
async fn load_signed_prekey(&self, id: u32) -> Result<Option<Vec<u8>>>;

/// Load all signed pre-keys (returns id, record pairs)
async fn load_all_signed_prekeys(&self) -> Result<Vec<(u32, Vec<u8>)>>;

/// Remove a signed pre-key
async fn remove_signed_prekey(&self, id: u32) -> Result<()>;
```

### Sender key operations

For group messaging encryption:

```rust theme={null}
/// Store a sender key for group messaging
async fn put_sender_key(&self, address: &str, record: &[u8]) -> Result<()>;

/// Get a sender key
async fn get_sender_key(&self, address: &str) -> Result<Option<Vec<u8>>>;

/// Delete a sender key
async fn delete_sender_key(&self, address: &str) -> Result<()>;
```

## AppSyncStore Trait

Handles WhatsApp app state synchronization storage.

### Sync key operations

```rust theme={null}
/// Get an app state sync key by ID
async fn get_sync_key(&self, key_id: &[u8]) -> Result<Option<AppStateSyncKey>>;

/// Set an app state sync key
async fn set_sync_key(&self, key_id: &[u8], key: AppStateSyncKey) -> Result<()>;

/// Get the latest sync key ID
async fn get_latest_sync_key_id(&self) -> Result<Option<Vec<u8>>>;
```

### Version Tracking

```rust theme={null}
/// Get the app state version for a collection
async fn get_version(&self, name: &str) -> Result<HashState>;

/// Set the app state version for a collection
async fn set_version(&self, name: &str, state: HashState) -> Result<()>;
```

### Mutation MAC Operations

```rust theme={null}
/// Store mutation MACs for a version
async fn put_mutation_macs(
    &self,
    name: &str,
    version: u64,
    mutations: &[AppStateMutationMAC],
) -> Result<()>;

/// Get a mutation MAC by index
async fn get_mutation_mac(&self, name: &str, index_mac: &[u8]) -> Result<Option<Vec<u8>>>;

/// Batch variant of get_mutation_mac: fetch many previous-MAC values in one
/// call, returning index_mac -> value_mac. The default loops over
/// get_mutation_mac; backends with set-membership queries (SQL `IN (...)`)
/// should override to avoid an N+1 (one round-trip per mutation) during sync.
async fn get_mutation_macs(
    &self,
    name: &str,
    index_macs: &[[u8; 32]],
) -> Result<HashMap<[u8; 32], Vec<u8>>>;

/// Delete mutation MACs by their index MACs
async fn delete_mutation_macs(&self, name: &str, index_macs: &[Vec<u8>]) -> Result<()>;
```

<Note>
  `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 custom backends that do **not** override it keep working without changes — override it with a `WHERE index_mac IN (…)` query for the performance win. The SQLite store chunks the `IN` list at 500 entries.

  An index MAC is always a full HMAC-SHA256 output, so this method's signature uses inline `[u8; 32]` arrays (`wacore::appstate_sync::IndexMac`) instead of `Vec<u8>` — zero per-MAC heap allocations on either side of the batch lookup. This is a breaking change for custom backends that override `get_mutation_macs`: update the parameter and return-map key types to `[u8; 32]`. `get_mutation_mac`, `put_mutation_macs`, and `delete_mutation_macs` are unaffected.
</Note>

## 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`).

```rust theme={null}
/// Get sender key distribution status for all known devices in a group.
/// Returns (device_jid_string, has_key) pairs.
async fn get_sender_key_devices(&self, group_jid: &str) -> Result<Vec<(String, bool)>>;

/// Set sender key status for devices. Use has_key=true after successful
/// SKDM distribution (WA Web: markHasSenderKey), or has_key=false to mark
/// devices as needing fresh SKDM (WA Web: markForgetSenderKey).
async fn set_sender_key_status(&self, group_jid: &str, entries: &[(&str, bool)]) -> Result<()>;

/// Clear all sender key device tracking for a group (on sender key rotation).
async fn clear_sender_key_devices(&self, group_jid: &str) -> Result<()>;
```

### LID-PN Mapping

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

```rust theme={null}
/// Get a mapping by LID
async fn get_lid_mapping(&self, lid: &str) -> Result<Option<LidPnMappingEntry>>;

/// Get a mapping by phone number (returns the most recent LID)
async fn get_pn_mapping(&self, phone: &str) -> Result<Option<LidPnMappingEntry>>;

/// Store or update a LID-PN mapping
async fn put_lid_mapping(&self, entry: &LidPnMappingEntry) -> Result<()>;

/// Get all LID-PN mappings (for cache warm-up)
async fn get_all_lid_mappings(&self) -> Result<Vec<LidPnMappingEntry>>;
```

### Base key collision detection

```rust theme={null}
/// Save the base key for a session address during retry collision detection
async fn save_base_key(&self, address: &str, message_id: &str, base_key: &[u8]) -> Result<()>;

/// Check if the current session has the same base key as the saved one
async fn has_same_base_key(
    &self,
    address: &str,
    message_id: &str,
    current_base_key: &[u8],
) -> Result<bool>;

/// Delete a base key entry
async fn delete_base_key(&self, address: &str, message_id: &str) -> Result<()>;
```

### Device Registry

```rust theme={null}
/// Update the device list for a user (called after usync responses)
async fn update_device_list(&self, record: DeviceListRecord) -> Result<()>;

/// Batched variant — update the device list for many users in one call,
/// used by the parallelized group encrypt fan-out so the device-registry
/// write doesn't serialize per-recipient.
async fn update_device_lists(&self, records: Vec<DeviceListRecord>) -> Result<()>;

/// Get all known devices for a user
async fn get_devices(&self, user: &str) -> Result<Option<DeviceListRecord>>;
```

### TcToken Storage

Trusted contact privacy tokens:

```rust theme={null}
/// Get a trusted contact token for a JID (stored under LID)
async fn get_tc_token(&self, jid: &str) -> Result<Option<TcTokenEntry>>;

/// Store or update a trusted contact token for a JID
async fn put_tc_token(&self, jid: &str, entry: &TcTokenEntry) -> Result<()>;

/// Delete a trusted contact token for a JID
async fn delete_tc_token(&self, jid: &str) -> Result<()>;

/// Get all JIDs that have stored tc tokens
async fn get_all_tc_token_jids(&self) -> Result<Vec<String>>;

/// Delete tc tokens that have no live state left. A row is removed only when
/// its received token is expired-or-absent (token_timestamp < token_cutoff, or
/// empty) AND its sender bucket is expired-or-absent (sender_timestamp <
/// sender_cutoff, or null) — so recent sender-side rate-limit state is never
/// dropped just because the received token expired. Returns count deleted.
async fn delete_expired_tc_tokens(&self, token_cutoff: i64, sender_cutoff: i64) -> Result<u32>;

/// Advance sender_timestamp for a contact toward `sender_timestamp`, inserting
/// a byte-less placeholder when no entry exists and preserving any existing
/// token bytes. The stored value only ever moves forward (max), so concurrent
/// writers (post-send issuance, history sync) converge regardless of ordering.
/// Must be atomic w.r.t. `put_tc_token`/`store_received_tc_token` — the default
/// is a read-modify-write; override with a single atomic upsert (`SqliteStore`
/// and `InMemoryBackend` both do).
async fn touch_tc_token_sender_timestamp(&self, jid: &str, sender_timestamp: i64) -> Result<()> {
    // default: read-modify-write via get_tc_token + put_tc_token
}

/// Store a token received from a contact, preserving any existing
/// `sender_timestamp`. The symmetric counterpart of
/// `touch_tc_token_sender_timestamp` — each writer owns its own field, so the
/// notification path never drops a sender bucket the issuance path wrote
/// concurrently.
///
/// **Newer-wins**: the stored `(token, token_timestamp)` pair is overwritten
/// only when the existing token is a byte-less placeholder, or the incoming
/// `token_timestamp` is at least as new as the stored one — a stale write must
/// never clobber a fresher real token. `SqliteStore` enforces this atomically
/// inside an `IMMEDIATE` transaction; `InMemoryBackend` under its state lock.
/// This is what lets concurrent history-sync chunks and the
/// privacy-notification path converge on the same row without an external
/// lock. The default read-modify-write below is a best-effort for
/// third-party backends — same atomicity caveat as
/// `touch_tc_token_sender_timestamp`.
async fn store_received_tc_token(&self, jid: &str, token: &[u8], token_timestamp: i64) -> Result<()> {
    // default: read-modify-write via get_tc_token + put_tc_token, newer-wins
}
```

<Note>
  `delete_expired_tc_tokens` gained a second `sender_cutoff` parameter — **this is a breaking change** for any custom backend that overrides it; update the signature to `(&self, token_cutoff: i64, sender_cutoff: i64) -> Result<u32>` and prune on both windows independently (see the built-in `SqliteStore`/`InMemoryBackend` implementations for the two-filter pattern).

  `touch_tc_token_sender_timestamp` and `store_received_tc_token` are defaulted methods — existing custom backends compile and work unchanged, but should override both with an atomic upsert if the backend supports one, since the default read-modify-write can race a concurrent writer touching the same row (post-send issuance vs. an incoming `privacy_token` notification). For `store_received_tc_token` specifically, a non-atomic override that races two callers can let an older token's write land last and clobber a fresher one — the built-in backends close this by making the newer-wins check part of the same read+write.
</Note>

### 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.

```rust theme={null}
/// Store a sent message's serialized payload for retry handling.
/// Called after each send_message(); the payload is the protobuf-encoded Message.
async fn store_sent_message(
    &self,
    chat_jid: &str,
    message_id: &str,
    payload: &[u8],
) -> Result<()>;

/// Retrieve and delete a sent message (atomic take). Returns serialized payload.
/// Called when a retry receipt arrives; consuming prevents double-retry.
async fn take_sent_message(
    &self,
    chat_jid: &str,
    message_id: &str,
) -> Result<Option<Vec<u8>>>;

/// Delete sent messages older than cutoff (unix timestamp seconds).
/// Returns count deleted.
async fn delete_expired_sent_messages(
    &self,
    cutoff_timestamp: i64,
) -> Result<u32>;
```

<Note>
  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.
</Note>

### MsgSecretStore

The fifth required member of [`Backend`](#the-backend-trait). 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](/api/bot#messagesecret-retention)).

```rust theme={null}
/// Store a single secret with no expiry (expires_at = 0, kept forever).
async fn put_msg_secret(
    &self,
    chat: &str,
    sender: &str,
    msg_id: &str,
    secret: &[u8; 32], // MessageSecret, the protocol-fixed message-secret size
) -> Result<()>;

/// Batch upsert. Each MsgSecretEntry carries its own absolute expires_at
/// deadline and parent message_ts. On conflict the later deadline wins
/// (0 = never) and the later non-zero message_ts wins. Returns count stored.
async fn put_msg_secrets(&self, entries: Vec<MsgSecretEntry>) -> Result<usize>;

/// Look up a secret for decryption.
async fn get_msg_secret(
    &self,
    chat: &str,
    sender: &str,
    msg_id: &str,
) -> Result<Option<Vec<u8>>>;

/// Look up a secret together with the parent message's event time, used to
/// enforce the per-add-on edit window.
async fn get_msg_secret_with_ts(
    &self,
    chat: &str,
    sender: &str,
    msg_id: &str,
) -> Result<Option<(Vec<u8>, i64)>>;

/// Delete secrets whose expires_at <= cutoff. Rows with expires_at = 0 are
/// kept forever. Returns count removed.
async fn delete_expired_msg_secrets(&self, cutoff_timestamp: i64) -> Result<u32>;
```

`get_msg_secret` and `get_msg_secret_with_ts` still return `Vec<u8>` rather than `MessageSecret`. Reads don't carry the same fixed-length guarantee as writes, since the persisted `secret BLOB` column has no length constraint at the SQL level.

```rust theme={null}
pub type MessageSecret = [u8; 32]; // wacore::reporting_token::MESSAGE_SECRET_SIZE

pub struct MsgSecretEntry {
    pub chat: Arc<str>,     // shared per conversation instead of one String per row
    pub sender: Arc<str>,
    pub msg_id: Arc<str>,
    pub secret: MessageSecret,
    pub expires_at: i64,
    pub message_ts: i64,
}
```

The `chat`, `sender`, and `msg_id` fields are `Arc<str>` rather than `String`. This allows buffered batch inserts to clone entries cheaply. The `secret` field uses the fixed-size `MessageSecret` array rather than `Vec<u8>`. This makes an invalid-length secret unrepresentable, so you no longer need a runtime length check.

This is a breaking change if you build `MsgSecretEntry` directly in a custom backend, or if you override the defaulted `put_msg_secret` method. If you construct entries directly, build the JID/ID fields with `Arc::from(...)` or `.into()`, and pass a `[u8; 32]` for `secret`. If you override `put_msg_secret` (most backends don't — see the Note below), update its signature to take `secret: &[u8; 32]` instead of `secret: &[u8]`. The SQLite table itself is unchanged:

```sql theme={null}
CREATE TABLE msg_secrets (
    chat TEXT NOT NULL,
    sender TEXT NOT NULL,
    msg_id TEXT NOT NULL,
    secret BLOB NOT NULL,
    device_id INTEGER NOT NULL DEFAULT 1,
    created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
    expires_at INTEGER NOT NULL DEFAULT 0,  -- absolute unix-seconds deadline (0 = never)
    message_ts INTEGER NOT NULL DEFAULT 0,  -- parent message event time (edit-window check)
    PRIMARY KEY (chat, sender, msg_id, device_id)
);
CREATE INDEX idx_msg_secrets_expires ON msg_secrets (device_id, expires_at);
```

<Note>
  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.
</Note>

## DeviceStore Trait

Handles device data persistence:

```rust theme={null}
/// Save device data
async fn save(&self, device: &Device) -> Result<()>;

/// Load device data
async fn load(&self) -> Result<Option<Device>>;

/// Check if a device exists
async fn exists(&self) -> Result<bool>;

/// Create a new device row and return its generated device_id
async fn create(&self) -> Result<i32>;

/// Create a snapshot of the database state
/// Optional: label with name, save extra_content (e.g. failing message)
async fn snapshot_db(&self, name: &str, extra_content: Option<&[u8]>) -> Result<()>;
```

#### resource\_report

```rust theme={null}
async fn resource_report(&self) -> wacore::stats::StorageResourceReport {
    wacore::stats::StorageResourceReport::default()
}
```

Best-effort process-local memory this backend attributes to the session. Defaulted — most custom backends don't need to implement it. It exists so a backend that can introspect its own memory (like `SqliteStore`'s SQLite page cache, often the single largest per-session chunk since it lives entirely outside the `Client`) can report it for [`Client::resource_report()`](/api/client#resource_report). Remote/store-backed backends (Redis, etc.) should override it to return `memory_bytes: Some(0)` — their data isn't process memory — rather than leaving the default all-`None` ("not reported"), if they want to make that positive claim explicit.

## 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:

```toml Cargo.toml theme={null}
[dependencies]
whatsapp-rust-sqlite-storage = { version = "0.6", default-features = false }
```

### Creating a store

```rust theme={null}
use whatsapp_rust::store::SqliteStore;

// Basic usage - creates/opens database at path
let store = SqliteStore::new("whatsapp.db").await?;

// With device_id for multi-device support
let store = SqliteStore::new_for_device("whatsapp.db", 1).await?;

// Using sqlite:// URL format
let store = SqliteStore::new("sqlite://path/to/db.sqlite").await?;
```

### Sharing the pool with sibling crates

```rust theme={null}
pub fn shared(&self) -> SharedSqlite;
```

`SqliteStore::shared()` returns a clonable `SharedSqlite` handle onto the store's existing r2d2 connection pool and write-serialization semaphore. A sibling crate uses it to run its own queries and migrations against the *same* database file — without opening a second connection pool, which would mean two WAL writers contending for the file lock. This is how [`whatsapp-rust-chat-store`](/api/chat-store) attaches its chat/message tables to the same `whatsapp.db` file as the device store.

```rust theme={null}
pub struct SharedSqlite { /* ... */ }

impl SharedSqlite {
    /// Acquires a semaphore permit and runs `f` on a pooled connection via `spawn_blocking`.
    pub async fn run<F, T>(&self, f: F) -> Result<T>
    where
        F: FnOnce(&mut SqliteConnection) -> Result<T> + Send + 'static,
        T: Send + 'static;
}
```

### 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:

```sql theme={null}
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 30000;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = 512;
PRAGMA temp_store = memory;
PRAGMA foreign_keys = ON;
```

`SqliteStoreConfig` also exposes an opt-in `mmap_size: Option<u64>` field (default `None`, current behavior — no `PRAGMA mmap_size` emitted). Set it with the builder-style `with_mmap_size(bytes)`:

```rust theme={null}
use whatsapp_rust_sqlite_storage::{SqliteStore, SqliteStoreConfig};

let config = SqliteStoreConfig::default().with_mmap_size(64 * 1024 * 1024); // 64 MiB
let store = SqliteStore::with_config("whatsapp.db", config).await?;
```

`with_mmap_size` only sets the field on the config value — the store applies it when the config is passed to [`SqliteStore::with_config`](#creating-a-store) (or `with_config_for_device`). Building a config and never passing it to one of those constructors leaves `mmap_size` unset, since `SqliteStore::new` / `new_for_device` always use `SqliteStoreConfig::default()`.

When set to a non-zero value, this emits `PRAGMA mmap_size = <bytes>;`, moving reads of the main database file through a reclaimable, OS-backed memory map instead of the heap page cache — useful for a process holding many small per-session databases, since mapped pages can be reclaimed under memory pressure while heap-cached pages cannot. `0` disables mmap, same as `None`.

<Warning>
  mmap I/O covers *reads* of the main database file only. In WAL mode (this store's default), writes still go through the WAL, and a checkpoint briefly falls back to non-mmap I/O. `SqliteStore::resource_report()` (see [`DeviceStore::resource_report`](#resource_report)) does not account for `mmap_size` — with mmap enabled, some reads bypass the heap page cache it measures, so the reported estimate can overstate actual process-heap residency for that session.
</Warning>

### Connection init hook

`SqliteStoreConfig` also exposes an optional `connection_init: Option<ConnectionInitHook>` field, set via the builder-style `with_connection_init(hook)`. The hook runs first in r2d2's `on_acquire` customizer on every pooled connection — before the store's own pragmas, and (because WAL setup and migrations also run on a pooled connection) before those too:

```rust theme={null}
pub type ConnectionInitHook = Arc<
    dyn Fn(&mut SqliteConnection) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>>
        + Send
        + Sync,
>;
```

The canonical use is SQLCipher-style keying, where `PRAGMA key` must be the first statement executed on a fresh connection, ideally followed by a verification query:

```rust theme={null}
use whatsapp_rust_sqlite_storage::{SqliteStore, SqliteStoreConfig};
use diesel::prelude::*;

let config = SqliteStoreConfig::default().with_connection_init(move |conn| {
    diesel::sql_query("PRAGMA key = 'my-passphrase';").execute(conn)?;
    // Verify the key: this fails on a wrongly-keyed database.
    diesel::sql_query("SELECT count(*) FROM sqlite_master;").execute(conn)?;
    Ok(())
});
let store = SqliteStore::with_config("whatsapp.db", config).await?;
```

Linking a SQLCipher-enabled SQLite is the caller's responsibility: disable this crate's default `bundled-sqlite` feature and depend on `libsqlite3-sys` with a SQLCipher build (e.g. its `bundled-sqlcipher` feature) instead. The crate itself gains no SQLCipher, key-type, or zeroization coupling — the hook is a generic per-connection seam that equally serves loading extensions or custom per-connection pragmas.

If the hook returns `Err`, the connection is rejected — this surfaces as a pool/build error at store construction (e.g. `SqliteStore::with_config` fails outright) rather than on a later query, which matters for a wrong-key error: it should fail fast, not after migrations already ran against an unreadable database.

<Note>
  The hook must be idempotent per connection and cheap: r2d2 calls it once for every connection it opens, including replacements after errors. `VACUUM INTO` snapshots run on a pooled (already-keyed) connection, so backups of an encrypted database stay encrypted with no extra work.
</Note>

### Usage Example

```rust theme={null}
use whatsapp_rust_sqlite_storage::SqliteStore;
use whatsapp_rust::Bot;
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create store
    let backend = Arc::new(SqliteStore::new("whatsapp.db").await?);
    
    // Use with Bot builder
    let mut bot = Bot::builder()
        .with_backend(backend.clone())
        .with_transport_factory(transport_factory)
        .with_http_client(http_client)
        .with_runtime(TokioRuntime)
        .on_event(|event, client| async move { /* handle events */ })
        .build()
        .await?;
    
    // Store implements all traits
    // SignalStore
    backend.put_identity("address@s.whatsapp.net", [0u8; 32]).await?;
    let identity = backend.load_identity("address@s.whatsapp.net").await?;
    
    // AppSyncStore
    let version = backend.get_version("regular").await?;
    
    // ProtocolStore
    let devices = backend.get_sender_key_devices("group@g.us").await?;
    
    // DeviceStore
    if backend.exists().await? {
        let device = backend.load().await?;
    }
    
    Ok(())
}
```

## CacheStore Trait

The `CacheStore` trait enables pluggable cache backends for the client's data caches. By default, caches use the in-process `PortableCache`; implementing this trait lets you use Redis, Memcached, or any other external cache.

**Location:** `wacore/src/store/cache.rs`

```rust theme={null}
#[async_trait]
pub trait CacheStore: Send + Sync + 'static {
    /// Retrieve a cached value by namespace and key.
    async fn get(&self, namespace: &str, key: &str) -> anyhow::Result<Option<Vec<u8>>>;

    /// Store a value with an optional TTL.
    /// When `ttl` is `None`, the entry persists until explicitly deleted.
    async fn set(
        &self,
        namespace: &str,
        key: &str,
        value: &[u8],
        ttl: Option<Duration>,
    ) -> anyhow::Result<()>;

    /// Delete a single key from the given namespace.
    async fn delete(&self, namespace: &str, key: &str) -> anyhow::Result<()>;

    /// Delete all keys in a namespace.
    async fn clear(&self, namespace: &str) -> anyhow::Result<()>;

    /// Approximate entry count (diagnostics only). Default returns 0.
    async fn entry_count(&self, _namespace: &str) -> anyhow::Result<u64> {
        Ok(0)
    }
}
```

### Namespaces

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

| 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 |

### 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

```rust theme={null}
pub struct CacheStores {
    pub group_cache: Option<Arc<dyn CacheStore>>,
    pub device_registry_cache: Option<Arc<dyn CacheStore>>,
    pub lid_pn_cache: Option<Arc<dyn CacheStore>>,
}
```

Set individual caches or use `CacheStores::all(store)` to route all pluggable caches to the same backend:

```rust theme={null}
use whatsapp_rust::{CacheConfig, CacheStores};

let redis = Arc::new(MyRedisCacheStore::new("redis://localhost:6379"));
let config = CacheConfig {
    cache_stores: CacheStores::all(redis),
    ..Default::default()
};
```

See [Custom backends — cache store](/guides/custom-backends#custom-cache-store) for a full implementation example.

## TypedCache

`TypedCache<K, V>` is a generic wrapper that dispatches to either the in-process `PortableCache` or a custom `CacheStore` backend.

**Location:** `src/cache_store.rs`

| Method              | Signature                                                                                           | Description                                                                                                     |
| ------------------- | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `from_local`        | `fn from_local(cache: Cache<K, V>) -> Self`                                                         | Wrap an in-process `PortableCache` (zero overhead)                                                              |
| `from_store`        | `fn from_store(store: Arc<dyn CacheStore>, namespace: &'static str, ttl: Option<Duration>) -> Self` | Create a cache backed by a custom store                                                                         |
| `get`               | `async fn get<Q>(&self, key: &Q) -> Option<V>`                                                      | Look up a value. Misses and deserialization failures return `None`                                              |
| `insert`            | `async fn insert(&self, key: K, value: V)`                                                          | Insert or update a value                                                                                        |
| `invalidate`        | `async fn invalidate<Q>(&self, key: &Q)`                                                            | Remove a single key                                                                                             |
| `invalidate_all`    | `fn invalidate_all(&self)`                                                                          | Remove all entries (sync). Best-effort for in-process backend; requires `tokio-runtime` for custom backends     |
| `clear`             | `async fn clear(&self)`                                                                             | Remove all entries (async). Awaits the write lock for in-process backend; awaits completion for custom backends |
| `run_pending_tasks` | `async fn run_pending_tasks(&self)`                                                                 | Evict expired entries (in-process backend only; no-op for custom backends)                                      |
| `entry_count`       | `fn entry_count(&self) -> u64`                                                                      | Approximate entry count (sync). Returns `0` for custom backends                                                 |
| `entry_count_async` | `async fn entry_count_async(&self) -> u64`                                                          | Approximate entry count, delegating to custom backend if available                                              |

<Warning>
  `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.
</Warning>

## 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

```rust theme={null}
use async_trait::async_trait;
use redis::aio::ConnectionManager;
use wacore::store::traits::*;
use wacore::store::error::Result;

pub struct RedisStore {
    client: ConnectionManager,
    device_id: i32,
}

impl RedisStore {
    pub async fn new(redis_url: &str) -> Result<Self> {
        let client = redis::Client::open(redis_url)
            .map_err(|e| StoreError::Connection(Box::new(e)))?;
        let conn = client.get_connection_manager().await
            .map_err(|e| StoreError::Connection(Box::new(e)))?;
        
        Ok(Self {
            client: conn,
            device_id: 1,
        })
    }
}

#[async_trait]
impl SignalStore for RedisStore {
    async fn put_identity(&self, address: &str, key: [u8; 32]) -> Result<()> {
        let mut conn = self.client.clone();
        let key_name = format!("identity:{}:{}", self.device_id, address);
        redis::cmd("SET")
            .arg(key_name)
            .arg(&key[..])
            .query_async(&mut conn)
            .await
            .map_err(|e| StoreError::Database(Box::new(e)))?;
        Ok(())
    }
    
    async fn load_identity(&self, address: &str) -> Result<Option<Vec<u8>>> {
        let mut conn = self.client.clone();
        let key_name = format!("identity:{}:{}", self.device_id, address);
        let result: Option<Vec<u8>> = redis::cmd("GET")
            .arg(key_name)
            .query_async(&mut conn)
            .await
            .map_err(|e| StoreError::Database(Box::new(e)))?;
        Ok(result)
    }
    
    // Implement remaining SignalStore methods...
}

#[async_trait]
impl AppSyncStore for RedisStore {
    // Implement all AppSyncStore methods...
}

#[async_trait]
impl ProtocolStore for RedisStore {
    // Implement all ProtocolStore methods...
}

#[async_trait]
impl DeviceStore for RedisStore {
    // Implement all DeviceStore methods...
}

// Backend is automatically implemented!
```

### 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

```rust theme={null}
pub struct AppStateSyncKey {
    pub key_data: Vec<u8>,
    pub fingerprint: Vec<u8>,
    pub timestamp: i64,
}
```

### LidPnMappingEntry

```rust theme={null}
pub struct LidPnMappingEntry {
    pub lid: String,              // LID user part
    pub phone_number: String,     // Phone number user part
    pub created_at: i64,          // Unix timestamp
    pub updated_at: i64,          // Unix timestamp
    pub learning_source: String,  // e.g. "usync", "peer_pn_message"
}
```

### TcTokenEntry

```rust theme={null}
pub struct TcTokenEntry {
    pub token: Vec<u8>,                    // Raw token bytes
    pub token_timestamp: i64,              // When token was received
    pub sender_timestamp: Option<i64>,     // When we sent our token
}
```

<Note>
  An entry with an empty `token` and only `sender_timestamp` set is a byte-less placeholder written by `touch_tc_token_sender_timestamp` — it records that a post-send issuance IQ succeeded before any real token had been received from the contact.
</Note>

### DeviceListRecord

```rust theme={null}
pub struct DeviceListRecord {
    pub user: String,               // User part of JID
    pub devices: Vec<DeviceInfo>,   // Known devices
    pub timestamp: i64,             // Last update timestamp
    pub phash: Option<String>,      // Participant hash from usync
    pub raw_id: Option<u32>,        // ADV raw_id for identity change detection
}

pub struct DeviceInfo {
    pub device_id: u32,           // 0 = primary, 1+ = companions
    pub key_index: Option<u32>,   // Key index if known
    pub is_hosted: bool,          // Hosted PN/LID address space (see below)
}
impl DeviceInfo {
    pub const fn new(device_id: u32, key_index: Option<u32>) -> Self
    pub const fn with_hosting(mut self, is_hosted: bool) -> Self
}
```

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).

<Note>
  **Breaking change:** `DeviceInfo` gained the `is_hosted` field (marks whether the device belongs to WhatsApp's hosted PN/LID address space, populated from usync device-list results). This breaks both construction and exhaustive pattern matching. Struct-literal construction (`DeviceInfo { device_id, key_index }`) no longer compiles — use `DeviceInfo::new(device_id, key_index).with_hosting(is_hosted)` instead. An exhaustive destructuring pattern (`let DeviceInfo { device_id, key_index } = info;`) also no longer compiles — add a `..` to the pattern or match on `is_hosted` as well. Persisted JSON without `is_hosted` still deserializes correctly (it defaults to `false`); only Rust construction and pattern-matching call sites are affected. See [USync](/api/usync#hosted-addressing) for how `is_hosted` is used with `Jid::with_device_hosting`.
</Note>

## 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:

```rust theme={null}
pub enum StoreError {
    Io(#[from] std::io::Error),
    Serialization(#[source] Box<dyn std::error::Error + Send + Sync>),
    Validation(String),
    Connection(#[source] Box<dyn std::error::Error + Send + Sync>),
    Database(#[source] Box<dyn std::error::Error + Send + Sync>),
    RetriesExhausted { op: String },
    Migration(#[source] Box<dyn std::error::Error + Send + Sync>),
    InvalidConfig(String),
    DeviceNotFound(i32),
}

pub type Result<T> = std::result::Result<T, StoreError>;
```

<Note>
  `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.
</Note>

## See also

* [Transport Trait](/api/transport) - Network transport abstraction
* [HTTP Client Trait](/api/http-client) - HTTP client abstraction
* [Client API](/api/client) - Main client interface
* [Chat Store](/api/chat-store) - Optional chat/message history built on `SqliteStore::shared()`
