Skip to main content
Each backend package exports a create*Store factory you pass into a backends entry of createStore. All backends implement the same per-domain store contracts, so switching backends is a config change, not a code change.

SQLite

@zapo-js/store-sqlite — createSqliteStore(config).
Provide exactly one of path or connection. With path, the library opens (and ref-counts) its own connection and closes it on store.destroy(). With connection, you own the lifecycle — store.destroy() leaves it open so you can keep using it elsewhere.

Bring your own connection

Share a single SQLite handle with the rest of your application by opening it yourself with openSqliteConnection and passing it through connection:
Requires the better-sqlite3 peer dependency.

PostgreSQL

@zapo-js/store-postgres — createPostgresStore(config).
Also exports createPgPool and ensurePgMigrations. Requires the pg peer dependency.

MySQL

@zapo-js/store-mysql — createMysqlStore(config).
Also exports createMysqlPool and ensureMysqlMigrations. Requires the mysql2 peer dependency.

Batch insert chunking

batchInsertChunkSize caps how many rows the PostgreSQL and MySQL backends fold into a single multi-row INSERT for batch writes — Signal sessions, remote identities, sender-key distributions, prekey generation, and the message/thread/contact upsertBatch paths used by write-behind persistence. The value is rounded down to the nearest power of two internally (500 → 256, 1000 → 512, …), and each batch is decomposed into power-of-two sub-chunks. That keeps the set of distinct prepared statements per connection bounded at log2(chunkSize) + 1 regardless of how N varies between calls — important for staying under the mysql2 client-side cache and MySQL’s max_prepared_stmt_count quota, and for keeping pg’s named statement cache stable. Leave the default unless benchmarks for your workload show it helps. Raise it for steady high-fanout group sends; lower it if your database limits are tight.

Redis

@zapo-js/store-redis — createRedisStore(config).

Per-domain store TTL

Persistent store domains never expire by default. Set storeTtlMs.<domain>Ms to make Redis prune that domain’s keys automatically — implemented as PEXPIRE on each write, no background cleanup needed. Any domain left unset stays persistent (byte-identical to the previous behavior).
Two semantics, by domain kind:
auth has no TTL knob on purpose: expiring login credentials would log the device out. Pick a crypto TTL longer than your worst idle window — an idle session beyond that window evicts the Signal / app-state keys and forces a re-handshake or re-sync. Non-positive ttlMs values are rejected at construction.
Requires the ioredis peer dependency.

MongoDB

@zapo-js/store-mongo — createMongoStore(config).
Requires the mongodb peer dependency. Bulk writes use { ordered: false } so independent upserts run in parallel — a per-document failure does not abort the rest of the batch.

Cache expiry and cleanup

The four cache domains (retry, groupMetadata, deviceList, messageSecret) carry a TTL set through cacheTtlMs. How expired entries are evicted differs per backend:
For PostgreSQL and MySQL, without startCleanup your cache tables grow monotonically. Reads still ignore expired rows so stale data is never served, but disk usage climbs forever. Start one poller per session id.
cleanup.intervalMs defaults to 60_000 (60s). result.destroy() stops every poller started through it, so calling poller.stop() yourself is only useful if you want to halt cleanup before tearing down the backend. For MongoDB, the TTL monitor’s ~60s latency means cache entries can linger past the configured TTL. Acceptable for groupMetadata/deviceList; switch to Redis if you need tighter eviction.

Mixing backends

createStore lets each domain choose a backend by name, so you can combine them:
Every persistence domain must be listed once backends is set — see Stores for the rule and the accepted values ('<backend>', 'memory', 'none').
Last modified on June 30, 2026