Skip to Content
Storage Adapters

Storage Adapters

Zaileys persists two completely independent pieces of state, and you can back each with a different adapter. Auth lives in an AuthStore (login credentials + Signal protocol keys) and chat data lives in a MessageStore (messages, chats, contacts, presence). They never share a backend unless you choose to, so you can keep your session in SQLite while streaming message history into Redis.

import { Client, SqliteAuthStore, RedisMessageStore } from 'zaileys' const client = new Client({ auth: new SqliteAuthStore({ database: './auth.db' }), store: new RedisMessageStore({ url: 'redis://localhost:6379' }), })

If you omit both options, Zaileys uses safe defaults — see Defaults. For where auth and store sit among the other client options, see Configuration.

The two store types

ConcernInterfaceWhat it holdsWhen it is written
SessionAuthStore (a.k.a. AuthStoreBundle)creds.json-style credentials + Signal keys (pre-keys, sessions, sender keys, app-state sync keys, …)On every login and key rotation, by Baileys
HistoryMessageStoreMessages, chats, contacts, presence (and optionally scheduled jobs)Continuously, via socket event listeners

AuthStore

An AuthStore is a bundle of two sub-stores:

interface AuthStoreBundle { readonly creds: AuthCredsStore // readCreds / writeCreds / deleteCreds readonly signal: AuthStore // read / write / delete / clear / close }

You pass an instance to Client({ auth }). Zaileys wires it into Baileys’ authentication state for you — you never call read/write yourself. On a logged-out disconnect (WhatsApp 401/410) Zaileys calls clear() so a stale session is wiped before the next login.

MessageStore

A MessageStore records the conversation as it streams in. After connecting, Zaileys calls store.bind(socket), which subscribes the store to Baileys events (messages.upsert, chats.upsert, contacts.upsert, presence.update, and where supported messages.update / chats.update). From then on you can query it:

import { Client, SqliteMessageStore } from 'zaileys' const store = new SqliteMessageStore({ database: './history.db' }) const client = new Client({ store }) client.on('connect', async () => { // newest-first, paginated chat history for one JID const recent = await store.listMessages('628123456789@s.whatsapp.net', { limit: 50 }) console.log(`loaded ${recent.length} messages`) const chats = await store.listChats({ archived: false }) const contacts = await store.listContacts() console.log(chats.length, 'chats,', contacts.length, 'contacts') })

Read methods every MessageStore exposes:

MethodReturnsNotes
getMessage(key)WAMessage | undefinedkey is a Baileys WAMessageKey
listMessages(jid, options?)WAMessage[]Newest-first; options.limit (default 100) and options.before (timestamp) for paging
getChat(jid) / listChats({ archived? })Chat / Chat[]archived filter optional
getContact(jid) / listContacts()Contact / Contact[]
getPresence(jid)PresenceData | undefined

JIDs follow WhatsApp conventions: individual chats are 628xxx@s.whatsapp.net and groups are xxx@g.us. listMessages('…@g.us', …) works the same for group history.

Defaults

If you do not pass auth / store, Zaileys picks:

OptionDefault adapterEffect
authFileAuthStore({ basePath: './.zaileys/auth/<sessionId>' })Session survives restarts, scoped per sessionId
storeMemoryMessageStore()History kept in RAM only — lost on restart
import { Client } from 'zaileys' // auth → ./.zaileys/auth/<sessionId>, store → in-memory const client = new Client({ sessionId: 'main' })
⚠️

The default message store is in-memory: chat history (and any scheduled jobs) vanish when the process exits. Pick a persistent store adapter if you need durable history or restart-safe scheduled broadcasts.

Available adapters

AdapterAuth storeMessage storePeer dependency
FileFileAuthStorenone
MemoryMemoryAuthStoreMemoryMessageStorenone
SQLiteSqliteAuthStoreSqliteMessageStorebetter-sqlite3
PostgresPostgresAuthStorePostgresMessageStorepg
RedisRedisAuthStoreRedisMessageStoreredis
ConvexConvexAuthStoreConvexMessageStoreconvex

⭐ = default for that store type. There is no file-backed message store — pair FileAuthStore with one of the other message stores (or MemoryMessageStore) if you want messages persisted.

Every non-built-in adapter loads its peer lazily. A missing peer throws a ZaileysStoreError with code STORE_NOT_AVAILABLE at first use (not at install/import time), so installs never break. Install peers like any other dependency — see Installation.

File (default auth)

Stores creds and Signal keys as JSON files under basePath, using atomic temp-file writes and BufferJSON so binary keys round-trip byte-for-byte. No peer dependency.

OptionTypeDefaultDescription
basePathstring'./.zaileys/auth'Directory for creds.json and signal/ key files
import { Client, FileAuthStore } from 'zaileys' const client = new Client({ auth: new FileAuthStore({ basePath: './.sessions/bot-1' }), })
⚠️

FileAuthStore is an auth store only. There is no FileMessageStore; if you do not set store, message history stays in memory.

Memory

Everything lives in process memory and is gone on exit. Ideal for tests, scratch scripts, or ephemeral workers. No peer dependency, no constructor options.

import { Client, MemoryAuthStore, MemoryMessageStore } from 'zaileys' const client = new Client({ auth: new MemoryAuthStore(), store: new MemoryMessageStore(), })
⚠️

MemoryAuthStore does not persist credentials — you re-scan the QR on every restart. Use it only when that is acceptable.

SQLite

Embedded single-file database via better-sqlite3 (WAL mode, prepared statements). Both stores auto-create their tables on first use. Buffers are stored as BLOBs serialized with BufferJSON.

Peer dependency: better-sqlite3

npm i better-sqlite3

SqliteAuthStore and SqliteMessageStore share the same options:

OptionTypeDefaultDescription
databasestring | Buffer— (required)Path to the .db file (or :memory:)
readonlybooleanfalseOpen the database read-only
import { Client, SqliteAuthStore, SqliteMessageStore } from 'zaileys' const client = new Client({ auth: new SqliteAuthStore({ database: './auth.db' }), store: new SqliteMessageStore({ database: './history.db' }), })

You can point both stores at the same database file — their tables (auth_creds, auth_signal, messages, chats, contacts, presence) do not collide.

Postgres

Backed by pg. Tables (zaileys_auth_creds, zaileys_auth_signal, zaileys_messages, zaileys_chats, zaileys_contacts, zaileys_presence) are created automatically with jsonb payloads and the right indexes.

Peer dependency: pg

npm i pg

Both PostgresAuthStore and PostgresMessageStore take:

OptionTypeDefaultDescription
connectionStringstringPostgres URL; the adapter creates and owns the pool
poolPool (from pg)A pre-built pool you own and close yourself
maxnumberpg defaultMax pool size (only when connectionString is used)
⚠️

connectionString and pool are mutually exclusive — pass exactly one. Passing both, or neither, throws a ZaileysStoreError (STORE_CONNECTION_FAILED).

// Connection string — the adapter owns the pool lifecycle import { Client, PostgresAuthStore, PostgresMessageStore } from 'zaileys' const conn = process.env.DATABASE_URL! const client = new Client({ auth: new PostgresAuthStore({ connectionString: conn, max: 5 }), store: new PostgresMessageStore({ connectionString: conn }), })
// Pre-built pool — you own and close it import { Pool } from 'pg' import { Client, PostgresAuthStore } from 'zaileys' const pool = new Pool({ connectionString: process.env.DATABASE_URL }) const client = new Client({ auth: new PostgresAuthStore({ pool }) }) // when the store owns the pool it closes it on shutdown; a pool you pass in is yours to end()

Redis

Backed by the redis client. Keys are namespaced (<namespace>:auth:*, <namespace>:msg:*, …) so multiple sessions can share one Redis instance. Messages use sorted sets for newest-first paging; presence entries get a short TTL.

Peer dependency: redis

npm i redis

Both RedisAuthStore and RedisMessageStore take:

OptionTypeDefaultDescription
urlstringRedis URL; the adapter creates, connects and owns the client
clientRedisClientTypeA pre-built, already-connected client you own
namespacestring'zaileys'Key prefix; isolates sessions sharing one server
⚠️

url and client are mutually exclusive — pass exactly one. If you pass a client, you must have already called await client.connect(); an unopened client throws STORE_CONNECTION_FAILED.

// URL — adapter manages the connection import { Client, RedisAuthStore, RedisMessageStore } from 'zaileys' const client = new Client({ auth: new RedisAuthStore({ url: 'redis://localhost:6379', namespace: 'wa-auth' }), store: new RedisMessageStore({ url: 'redis://localhost:6379', namespace: 'wa-store' }), })
// Shared, pre-connected client import { createClient } from 'redis' import { Client, RedisAuthStore, RedisMessageStore } from 'zaileys' const redis = createClient({ url: 'redis://localhost:6379' }) await redis.connect() const client = new Client({ auth: new RedisAuthStore({ client: redis, namespace: 'wa-auth' }), store: new RedisMessageStore({ client: redis, namespace: 'wa-store' }), })
⚠️

clear() only removes keys within the store’s namespace. When auth and store share one Redis instance, give them distinct namespaces so wiping one (e.g. on logout) does not delete the other.

Convex

Convex is a hosted reactive backend reached through deployed functions, so unlike the other adapters it needs a one-time deploy step. Both stores talk to a single zaileys_kv table through the functions zaileys:get|set|del|clear|list.

Peer dependency: convex

Deploy the functions

In your Convex project, merge the zaileys_kv table from examples/convex/schema.ts into your convex/schema.ts, copy examples/convex/zaileys.ts into your project as convex/zaileys.ts, then deploy:

npx convex dev # or: npx convex deploy

Install the peer

npm i convex

Wire it into the Client

import { Client, ConvexAuthStore, ConvexMessageStore } from 'zaileys' const url = process.env.CONVEX_URL // e.g. https://your-deployment.convex.cloud const client = new Client({ auth: new ConvexAuthStore({ url, namespace: 'wa-auth' }), store: new ConvexMessageStore({ url, namespace: 'wa-store' }), }) client.on('qr', ({ qrString }) => console.log('Scan QR:', qrString)) client.on('connect', ({ me }) => console.log('Connected as', me.id, '— session in Convex'))

Both ConvexAuthStore and ConvexMessageStore take:

OptionTypeDefaultDescription
urlstringConvex deployment URL; the adapter builds a ConvexHttpClient
clientConvexClientLikeA pre-built Convex client (e.g. ConvexHttpClient) you own
namespacestring'zaileys'Logical partition inside zaileys_kv

Pass a pre-built client instead of a url if you already have one:

import { ConvexHttpClient } from 'convex/browser' import { ConvexAuthStore, ConvexMessageStore } from 'zaileys' const convex = new ConvexHttpClient(process.env.CONVEX_URL!) const auth = new ConvexAuthStore({ client: convex, namespace: 'wa-auth' }) const store = new ConvexMessageStore({ client: convex, namespace: 'wa-store' })
⚠️

Use distinct namespace values for auth vs store when they share one deployment — clear() wipes a whole namespace, and auth clear() runs on a 401/410 logout. The full deploy walkthrough lives in examples/convex/README.md and the runnable script in examples/convex-store.ts.

Scheduled-job persistence

Scheduled broadcasts (client.schedule(...)) store a ScheduledJobRecord so pending sends can be re-armed after a restart. This relies on the message store implementing the optional saveScheduledJob / listScheduledJobs / deleteScheduledJob methods.

Message storePersists scheduled jobs?
ConvexMessageStore✅ Yes
MemoryMessageStore❌ In-memory only
SqliteMessageStore❌ In-memory only
PostgresMessageStore❌ In-memory only
RedisMessageStore❌ In-memory only
⚠️

Only ConvexMessageStore implements the scheduled-job methods today. With every other message store, scheduled jobs are held in process memory and are lost on restart — the scheduler simply falls back to its in-memory map. If you need restart-safe schedules, use the Convex store.

Mixing auth and store adapters

Because the two stores are independent you can mix freely — there is no requirement that they share a backend.

import { Client, FileAuthStore, RedisMessageStore } from 'zaileys' // Session on disk, history in Redis const client = new Client({ auth: new FileAuthStore({ basePath: './.zaileys/auth/main' }), store: new RedisMessageStore({ url: 'redis://localhost:6379', namespace: 'wa' }), })
import { Pool } from 'pg' import { createClient } from 'redis' import { Client, PostgresAuthStore, RedisMessageStore } from 'zaileys' const pool = new Pool({ connectionString: process.env.DATABASE_URL }) const redis = createClient({ url: 'redis://localhost:6379' }) await redis.connect() // Durable auth in Postgres, fast history in Redis const client = new Client({ auth: new PostgresAuthStore({ pool }), store: new RedisMessageStore({ client: redis, namespace: 'wa-store' }), })

Tips and gotchas

  • Buffers round-trip safely. Every adapter serializes with BufferJSON, so binary Signal keys and media references survive storage byte-for-byte.
  • Errors are typed. Failures throw ZaileysStoreError with a code such as STORE_NOT_AVAILABLE (missing peer), STORE_CONNECTION_FAILED, STORE_READ_FAILED, STORE_WRITE_FAILED, STORE_CORRUPTED, or STORE_CLOSED. See Error handling.
  • Owned vs. provided clients. When you pass a url / connectionString, the adapter creates and closes the connection for you. When you pass a client / pool, the lifecycle is yours.
  • listMessages paging. It returns newest-first; pass before (a message timestamp) plus limit to walk backwards through history.
  • Per-session scoping. The default FileAuthStore path includes sessionId, so multiple clients in one process do not clash. With Redis/Convex use distinct namespace values to achieve the same.

See also: Configuration · Broadcast & Schedule · Installation.

Last updated on