zapo persists everything a session needs to survive a restart: pairing credentials, Signal protocol state, app-state collections, and optionally your message/thread/contact archive. You build one with createStore and pass it to the client.
The model
createStore separates backends (where data lives) from providers (which backend each domain uses). This lets you mix backends — e.g. keep hot signal state in Redis while archiving messages in Postgres.
Providers are required when you set backends
As soon as backends contains at least one entry, every persistence domain must be assigned explicitly in providers. The required domains are auth, signal, preKey, session, identity, senderKey, appState, privacyToken, messages, threads, and contacts. Both the TypeScript types and a runtime check enforce this — createStore throws and lists the missing providers.* keys when any are omitted.
Three values are valid for each domain:
- A backend name from
backends(e.g.'sqlite') — persist that domain there. 'memory'— keep that domain in the in-tree memory provider for this run.'none'— only valid for the optional archive domains (messages,threads,contacts); skips the domain entirely.
auth and let Signal state, app-state, or the mailbox fall back to memory, the device pairs once and then loses its protocol state on every restart. Pick 'memory' deliberately when that is what you want.
backends is empty or omitted, every domain falls back to memory (mailbox domains to 'none') — useful for tests, but the device re-pairs on every restart.
Persisted domains
These hold the state required to keep a session alive. Back them with a durable backend in production.Optional archive domains
These accept'none' to disable persistence entirely:
Cache domains
Configured undercacheProviders and default to bounded memory with TTLs:
memory runs an in-process sweep, Redis and MongoDB use native TTL, SQLite filters on read, and PostgreSQL/MySQL require an opt-in poller (result.startCleanup(sessionId)) or cache tables grow forever. See Cache expiry and cleanup for the per-backend matrix.
Read-through cache layer
When a hot signal domain points at a persistent backend, every send/recv round-trip pays the backend’s latency to fetch the same peer’s session, identity, or sender key. ThecacheLayer option wraps the backend store with a bounded-LRU L1 (the in-tree memory provider) so repeated reads of the same peer skip the backend, while writes stay write-through so the backend remains authoritative.
Four hot domains can be cached:
All flags default to
false. A flag is a no-op unless that domain resolves to a real backend in providers — caching 'memory' or 'none' in front of itself buys nothing and is skipped.
limits caps per-domain entry counts; once exceeded, the L1 evicts LRU. When unset, each domain defaults to the matching memory-provider cap.
When to enable it
Turn it on when your backend is a network hop (Redis, Postgres, MySQL, MongoDB) and you send or receive at a rate where the same peers repeat — typical for bots, group fan-out, and multi-tenant gateways. With a local SQLite backend the wins are smaller; measure before flipping it on.Single-writer assumption
The L1 is per-process and has no cross-process invalidation channel. EnablecacheLayer only when a single process owns a given sessionId’s backend rows — the library’s standard connection model. Different sessions sharing one backend are fine; the same session opened from two processes is not.
Why not every domain?
signal, appState, and preKey are deliberately excluded:
signal— the per-send registration read is already memoized inside the signal lock; a second cache adds nothing.appState— the sync client already caches collection state for the sync-context lifetime, the only scope where reads both repeat and stay coherent.preKey— one-time pre-keys are read exactly once then consumed. Serving a consumed key from a stale cache would reuse it and break forward secrecy.
Backends
SQLite
@zapo-js/store-sqlite — local, single-process.PostgreSQL
@zapo-js/store-postgres — distributed, relational.MySQL
@zapo-js/store-mysql — distributed, relational.Redis
@zapo-js/store-redis — cache + persistence.MongoDB
@zapo-js/store-mongo — document store.Memory
Built in. Great for tests; does not survive a restart.
Memory-only (tests)
For quick experiments or tests, omitbackends entirely — every domain falls back to memory:
