Persistence adapter behaviour for WhatsApp session state.
Adapters implement this behaviour to persist session data including:
- Noise protocol keys (static keypair, handshake hash/salt)
- Signal protocol state (identity, signed prekeys, sessions)
- Client configuration and pairing data
- App state sync data:
:app_state_keys:app_state_versions:app_state_mutation_macs:contacts
Built-in Adapters
ExWapp.Store.Ets- ETS table with file persistence (default)ExWapp.Store.Memory- In-memory only, useful for testing
Passing a Store to Session
# Use default ETS adapter
ExWapp.connect(store: nil)
# Use ETS with custom path
ExWapp.connect(store: {ExWapp.Store.Ets, path: "/custom/path.etf"})
# Use memory adapter for testing
ExWapp.connect(store: ExWapp.Store.Memory)
# Use pre-created store instance
store = ExWapp.Store.Ets.new(path: "my_session.etf")
ExWapp.connect(store: store)Creating a Custom Adapter
Implement the ExWapp.Store behaviour:
defmodule MyApp.PostgresStore do
@behaviour ExWapp.Store
defstruct [:conn, :user_id]
@impl true
def new(opts) do
conn = Keyword.fetch!(opts, :conn)
user_id = Keyword.fetch!(opts, :user_id)
%__MODULE__{conn: conn, user_id: user_id}
end
@impl true
def load(%__MODULE__{conn: conn, user_id: user_id}) do
case MyApp.Repo.get_by(WaSession, user_id: user_id) do
nil -> {:ok, %{}}
session -> {:ok, session.data}
end
end
@impl true
def persist(%__MODULE__{conn: conn, user_id: user_id}, data) do
MyApp.Repo.insert_or_update!(%WaSession{user_id: user_id, data: data})
:ok
end
end
Summary
Callbacks
Begins a caller-owned persistence batch.
Optional callback to clear all stored data.
Optional callback that releases adapter-owned processes or connections.
Deletes every message associated with one chat.
Deletes selected messages after durable application persistence.
Finds one message in a chat by ID.
Finds one message by ID across all chats.
Ends a caller-owned persistence batch and durably flushes pending writes.
Durably flushes all pending writes without closing the adapter.
Atomically updates one domain and returns a value from the update.
Loads persisted session data.
Creates a new store instance.
Persists session data.
Reads a consistent snapshot of non-specialized top-level store data.
Materializes all messages for an explicit backup operation.
Returns a lazy cursor over messages stored for one chat.
Atomically transforms all non-specialized top-level store data.
Atomically updates one top-level store domain.
Updates the status of one message in a chat.
Updates the status of every message matching an ID.
Upserts messages without embedding them in chat metadata.
Functions
Runs a function inside a caller-owned persistence batch.
Starts a caller-owned write batch when supported by the adapter.
Clears all data from the store.
Releases resources held by a store adapter when it implements close/1.
Deletes all messages associated with one chat.
Deletes selected messages after the caller has persisted them durably.
Finds one stored message in a chat.
Finds one stored message by ID across all chats.
Finishes a caller-owned write batch and flushes it when supported.
Durably flushes pending adapter writes when the adapter supports it.
Reads a single top-level key from the store.
Atomically updates one top-level domain and returns the updater's reply.
Materializes a single page from the message stream.
Loads data from a store adapter.
Loads data from store, returning empty map on error.
Loads the recent device cache snapshot.
Loads Signal session state from the store.
Reports which optional callbacks an adapter does not implement.
Creates a store instance from various input formats.
Persists data via the store adapter.
Persists the recent device cache snapshot.
Persists Signal session state back to the store.
Stores one top-level domain atomically.
Reads the adapter's non-specialized data through its write owner when supported.
Returns a complete local snapshot including separately stored messages.
Builds a lazy message stream for a chat.
Atomically transforms the adapter's non-specialized top-level data.
Atomically updates one top-level domain without a load/modify/persist race.
Updates one stored message status.
Updates message status by ID across all chats.
Inserts or replaces messages by ID in the dedicated message store.
Types
Callbacks
Begins a caller-owned persistence batch.
Optional callback to clear all stored data.
Useful for testing or when unlinking a device.
Optional callback that releases adapter-owned processes or connections.
Sessions call this only for store instances they created themselves. A pre-created store passed as a struct remains owned by the caller.
Deletes every message associated with one chat.
@callback delete_messages(store :: t(), jid :: String.t(), message_ids :: [String.t()]) :: :ok | {:error, term()}
Deletes selected messages after durable application persistence.
Finds one message in a chat by ID.
Finds one message by ID across all chats.
Ends a caller-owned persistence batch and durably flushes pending writes.
Durably flushes all pending writes without closing the adapter.
@callback get_and_update( store :: t(), key :: atom(), default :: term(), (term() -> {term(), term()}) ) :: term() | {:error, term()}
Atomically updates one domain and returns a value from the update.
Loads persisted session data.
Returns {:ok, data} with the stored map, or {:ok, %{}} if no data exists.
Returns {:error, reason} if the load operation fails.
Creates a new store instance.
Options are adapter-specific. See the adapter module documentation.
Persists session data.
The data map contains all session state that needs to survive restarts.
Returns :ok on success or {:error, reason} on failure.
Reads a consistent snapshot of non-specialized top-level store data.
Materializes all messages for an explicit backup operation.
@callback stream_messages(store :: t(), jid :: String.t(), opts :: keyword()) :: Enumerable.t()
Returns a lazy cursor over messages stored for one chat.
Atomically transforms all non-specialized top-level store data.
@callback update(store :: t(), key :: atom(), default :: term(), (term() -> term())) :: :ok | {:error, term()}
Atomically updates one top-level store domain.
@callback update_message_status( store :: t(), jid :: String.t(), message_id :: String.t(), status :: atom() ) :: :ok | {:error, term()}
Updates the status of one message in a chat.
@callback update_message_status_by_id( store :: t(), message_id :: String.t(), status :: atom() ) :: :ok | {:error, term()}
Updates the status of every message matching an ID.
@callback upsert_messages(store :: t(), jid :: String.t(), messages :: [map()]) :: :ok | {:error, term()}
Upserts messages without embedding them in chat metadata.
Functions
Runs a function inside a caller-owned persistence batch.
Starts a caller-owned write batch when supported by the adapter.
Clears all data from the store.
Only available if the adapter implements the optional clear/1 callback.
Releases resources held by a store adapter when it implements close/1.
Adapters without process or connection resources need no callback.
Deletes all messages associated with one chat.
Deletes selected messages after the caller has persisted them durably.
Finds one stored message in a chat.
Finds one stored message by ID across all chats.
Finishes a caller-owned write batch and flushes it when supported.
Durably flushes pending adapter writes when the adapter supports it.
Reads a single top-level key from the store.
Adapters may override this with a more efficient implementation that avoids loading and merging the entire persisted store.
@spec get_and_update(t(), atom(), term(), (term() -> {term(), term()})) :: term() | {:error, term()}
Atomically updates one top-level domain and returns the updater's reply.
Materializes a single page from the message stream.
Loads data from a store adapter.
Examples
iex> store = ExWapp.Store.Memory.new([])
iex> ExWapp.Store.load(store)
{:ok, %{}}
Loads data from store, returning empty map on error.
This is a convenience function that never fails. Use load/1 if you
need to handle errors explicitly.
Loads the recent device cache snapshot.
Loads Signal session state from the store.
ETS-backed adapters may return a lightweight session-store reference instead of a plain map so callers can avoid keeping large session state on process heaps.
Reports which optional callbacks an adapter does not implement.
Every message-level callback is optional and dispatched through
function_exported?/3, so an adapter that misspells one, or never wrote it,
silently falls through to a generic path with different semantics — no
compile warning, no runtime error, just behaviour that quietly diverges.
Naming the gap once at session start turns that into something a reader can
see. Returns the missing {function, arity} pairs, newest adapters usually
returning [].
Creates a store instance from various input formats.
Examples
# Use default adapter
iex> ExWapp.Store.new(nil)
%ExWapp.Store.Ets{...}
# Use module name
iex> ExWapp.Store.new(ExWapp.Store.Memory)
%ExWapp.Store.Memory{...}
# Use module with options
iex> ExWapp.Store.new({ExWapp.Store.Ets, path: "/tmp/test.etf"})
%ExWapp.Store.Ets{path: "/tmp/test.etf", ...}
# Pass existing struct
iex> store = ExWapp.Store.Memory.new([])
iex> ExWapp.Store.new(store) == store
true
Persists data via the store adapter.
Examples
iex> store = ExWapp.Store.Memory.new([])
iex> ExWapp.Store.persist(store, %{foo: "bar"})
:ok
iex> ExWapp.Store.load(store)
{:ok, %{foo: "bar"}}
Persists the recent device cache snapshot.
Persists Signal session state back to the store.
Stores one top-level domain atomically.
Reads the adapter's non-specialized data through its write owner when supported.
Returns a complete local snapshot including separately stored messages.
This is intentionally eager and should only be used for explicit backups. Normal consumers should use stream_messages/3.
@spec stream_messages(t(), String.t(), keyword()) :: Enumerable.t()
Builds a lazy message stream for a chat.
Built-in adapters keep message payloads outside chat metadata. Custom adapters can implement stream_messages/3 with a database cursor; adapters that do not implement it use a compatibility fallback over :chat_messages.
Atomically transforms the adapter's non-specialized top-level data.
Prefer update/4 for a single domain because it avoids materializing the
rest of the store.
Atomically updates one top-level domain without a load/modify/persist race.
Built-in adapters execute updater inside their write owner. Custom adapters
can implement update/4; the compatibility fallback cannot guarantee
serialization across callers.
Updates one stored message status.
Updates message status by ID across all chats.
Inserts or replaces messages by ID in the dedicated message store.