Baileys does not include built-in persistent storage for messages, chats, or contacts. You need to implement your own data store to maintain chat history and contact information.
Why You Need a Store
Without a data store:
- Message history is lost when your application restarts
- You can’t query past messages
- Chat metadata is not persisted
- Contact information must be refetched
For production applications, implement a database-backed store. In-memory stores are not recommended for production use as they store everything in RAM and data is lost on restart.
Store Requirements
A complete store should handle:
- Messages - Store and retrieve message history
- Chats - Persist chat metadata and unread counts
- Contacts - Save contact names and profile info
- Groups - Cache group metadata and participants
Basic In-Memory Implementation
Here’s a simple in-memory store example:
import { WAMessage, Chat, Contact } from '@whiskeysockets/baileys'
class SimpleStore {
public messages: Map<string, WAMessage[]> = new Map()
public chats: Map<string, Chat> = new Map()
public contacts: Map<string, Contact> = new Map()
constructor(private sock: ReturnType<typeof makeWASocket>) {
this.bind()
}
private bind() {
// Store new messages
this.sock.ev.on('messages.upsert', ({ messages }) => {
for (const msg of messages) {
const jid = msg.key.remoteJid!
const existing = this.messages.get(jid) || []
existing.push(msg)
this.messages.set(jid, existing)
}
})
// Store chats
this.sock.ev.on('chats.upsert', (chats) => {
for (const chat of chats) {
this.chats.set(chat.id, chat)
}
})
// Update chats
this.sock.ev.on('chats.update', (updates) => {
for (const update of updates) {
const chat = this.chats.get(update.id!)
if (chat) {
Object.assign(chat, update)
}
}
})
// Store contacts
this.sock.ev.on('contacts.upsert', (contacts) => {
for (const contact of contacts) {
this.contacts.set(contact.id, contact)
}
})
}
getMessages(jid: string): WAMessage[] {
return this.messages.get(jid) || []
}
getMessage(key: WAMessageKey): WAMessage | undefined {
const messages = this.messages.get(key.remoteJid!)
return messages?.find(m => m.key.id === key.id)
}
getChat(jid: string): Chat | undefined {
return this.chats.get(jid)
}
}
// Usage
const sock = makeWASocket({ /* config */ })
const store = new SimpleStore(sock)
Database-Backed Store
For production, use a database like PostgreSQL, MongoDB, or SQLite:
import { WAMessage, Chat, Contact } from '@whiskeysockets/baileys'
import { Database } from 'your-db-library'
class DatabaseStore {
constructor(
private db: Database,
private sock: ReturnType<typeof makeWASocket>
) {
this.bind()
}
private bind() {
// Store messages in database
this.sock.ev.on('messages.upsert', async ({ messages }) => {
for (const msg of messages) {
await this.db.messages.insert({
id: msg.key.id,
remoteJid: msg.key.remoteJid,
fromMe: msg.key.fromMe,
participant: msg.key.participant,
timestamp: msg.messageTimestamp,
message: JSON.stringify(msg.message),
pushName: msg.pushName,
broadcast: msg.broadcast
})
}
})
// Store chats
this.sock.ev.on('chats.upsert', async (chats) => {
for (const chat of chats) {
await this.db.chats.upsert({
id: chat.id,
name: chat.name,
unreadCount: chat.unreadCount,
conversationTimestamp: chat.conversationTimestamp,
data: JSON.stringify(chat)
})
}
})
// Update chats
this.sock.ev.on('chats.update', async (updates) => {
for (const update of updates) {
await this.db.chats.update(
{ id: update.id },
update
)
}
})
// Delete messages
this.sock.ev.on('messages.delete', async (deletion) => {
if ('keys' in deletion) {
for (const key of deletion.keys) {
await this.db.messages.delete({
id: key.id,
remoteJid: key.remoteJid
})
}
} else {
await this.db.messages.deleteMany({
remoteJid: deletion.jid
})
}
})
}
async getMessage(key: WAMessageKey): Promise<WAMessage | undefined> {
const row = await this.db.messages.findOne({
id: key.id,
remoteJid: key.remoteJid
})
if (!row) return undefined
return {
key: {
id: row.id,
remoteJid: row.remoteJid,
fromMe: row.fromMe,
participant: row.participant
},
message: JSON.parse(row.message),
messageTimestamp: row.timestamp,
pushName: row.pushName
}
}
async getMessages(jid: string, limit = 50): Promise<WAMessage[]> {
const rows = await this.db.messages.find(
{ remoteJid: jid },
{ limit, orderBy: { timestamp: 'DESC' } }
)
return rows.map(row => ({
key: {
id: row.id,
remoteJid: row.remoteJid,
fromMe: row.fromMe
},
message: JSON.parse(row.message),
messageTimestamp: row.timestamp
}))
}
}
Implementing getMessage
Many Baileys features require a getMessage function to retrieve messages from your store:
import makeWASocket from '@whiskeysockets/baileys'
const store = new DatabaseStore(db, sock)
const sock = makeWASocket({
// Provide getMessage for:
// - Message retries
// - Poll vote decryption
// - Quote message handling
getMessage: async (key) => {
return await store.getMessage(key)
}
})
Why getMessage is Important
Message retries
When message delivery fails, WhatsApp may request a retry. Baileys needs the original message to resend it.
Poll votes
Poll votes are encrypted and reference the original poll message. You need getMessage to decrypt votes.sock.ev.on('messages.update', async (updates) => {
for (const { key, update } of updates) {
if (update.pollUpdates) {
const pollMessage = await getMessage(key)
if (pollMessage) {
const votes = getAggregateVotesInPollMessage({
message: pollMessage,
pollUpdates: update.pollUpdates
})
console.log('Poll results:', votes)
}
}
}
})
Forward messages
When forwarding messages, Baileys needs access to the original message content.
Messages with media (images, videos, documents) should store media references:
sock.ev.on('messages.upsert', async ({ messages }) => {
for (const msg of messages) {
if (msg.message?.imageMessage) {
const imageMsg = msg.message.imageMessage
// Store media reference
await db.media.insert({
messageId: msg.key.id,
mediaKey: imageMsg.mediaKey,
directPath: imageMsg.directPath,
url: imageMsg.url,
mimetype: imageMsg.mimetype,
fileLength: imageMsg.fileLength,
caption: imageMsg.caption
})
}
}
})
Group operations are faster when you cache group metadata:
import NodeCache from '@cacheable/node-cache'
const groupCache = new NodeCache({
stdTTL: 5 * 60, // 5 minutes
useClones: false
})
const sock = makeWASocket({
cachedGroupMetadata: async (jid) => groupCache.get(jid)
})
// Update cache when groups change
sock.ev.on('groups.update', async ([event]) => {
const metadata = await sock.groupMetadata(event.id)
groupCache.set(event.id, metadata)
})
sock.ev.on('group-participants.update', async (event) => {
const metadata = await sock.groupMetadata(event.id)
groupCache.set(event.id, metadata)
})
Store Events to Listen
Implement handlers for all these events:
class CompletePersistentStore {
private bind() {
// Messages
this.sock.ev.on('messages.upsert', this.handleMessagesUpsert)
this.sock.ev.on('messages.update', this.handleMessagesUpdate)
this.sock.ev.on('messages.delete', this.handleMessagesDelete)
// Chats
this.sock.ev.on('chats.upsert', this.handleChatsUpsert)
this.sock.ev.on('chats.update', this.handleChatsUpdate)
this.sock.ev.on('chats.delete', this.handleChatsDelete)
// Contacts
this.sock.ev.on('contacts.upsert', this.handleContactsUpsert)
this.sock.ev.on('contacts.update', this.handleContactsUpdate)
// Groups
this.sock.ev.on('groups.upsert', this.handleGroupsUpsert)
this.sock.ev.on('groups.update', this.handleGroupsUpdate)
// History
this.sock.ev.on('messaging-history.set', this.handleHistorySet)
}
}
Example: PostgreSQL Schema
CREATE TABLE messages (
id VARCHAR(255) NOT NULL,
remote_jid VARCHAR(255) NOT NULL,
from_me BOOLEAN DEFAULT FALSE,
participant VARCHAR(255),
timestamp BIGINT,
message JSONB,
push_name VARCHAR(255),
PRIMARY KEY (id, remote_jid)
);
CREATE INDEX idx_messages_jid ON messages(remote_jid, timestamp DESC);
CREATE TABLE chats (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255),
unread_count INTEGER DEFAULT 0,
conversation_timestamp BIGINT,
archived BOOLEAN DEFAULT FALSE,
pinned BOOLEAN DEFAULT FALSE,
data JSONB
);
CREATE TABLE contacts (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255),
notify VARCHAR(255),
verified_name VARCHAR(255),
img_url TEXT,
status TEXT
);
Example: MongoDB Schema
import { Schema, model } from 'mongoose'
const MessageSchema = new Schema({
_id: String, // key.id
remoteJid: { type: String, index: true },
fromMe: Boolean,
participant: String,
timestamp: { type: Number, index: true },
message: Schema.Types.Mixed,
pushName: String
})
const ChatSchema = new Schema({
_id: String, // chat.id
name: String,
unreadCount: { type: Number, default: 0 },
conversationTimestamp: Number,
archived: Boolean,
pinned: Boolean,
data: Schema.Types.Mixed
})
const ContactSchema = new Schema({
_id: String, // contact.id
name: String,
notify: String,
verifiedName: String,
imgUrl: String,
status: String
})
export const Message = model('Message', MessageSchema)
export const Chat = model('Chat', ChatSchema)
export const Contact = model('Contact', ContactSchema)
Message Deduplication
Implement deduplication to avoid storing duplicate messages:
sock.ev.on('messages.upsert', async ({ messages }) => {
for (const msg of messages) {
// Check if message already exists
const exists = await db.messages.findOne({
id: msg.key.id,
remoteJid: msg.key.remoteJid
})
if (!exists) {
await db.messages.insert({
id: msg.key.id,
remoteJid: msg.key.remoteJid,
message: msg.message,
timestamp: msg.messageTimestamp
})
}
}
})
Store Best Practices
Follow these best practices when implementing your store:
- Use indexes - Index
remoteJid and timestamp for fast queries
- Handle duplicates - Messages may arrive multiple times during sync
- Implement cleanup - Delete old messages to manage storage
- Batch operations - Use batch inserts for history sync
- Error handling - Don’t let store errors crash your application
- Backup regularly - Message data is critical for your application
Complete Store Example
import makeWASocket, { WAMessage, WAMessageKey } from '@whiskeysockets/baileys'
class MessageStore {
private messages = new Map<string, WAMessage[]>()
constructor(sock: ReturnType<typeof makeWASocket>) {
sock.ev.on('messages.upsert', ({ messages }) => {
this.addMessages(messages)
})
sock.ev.on('messages.update', (updates) => {
this.updateMessages(updates)
})
sock.ev.on('messages.delete', (deletion) => {
this.deleteMessages(deletion)
})
}
private addMessages(messages: WAMessage[]) {
for (const msg of messages) {
const jid = msg.key.remoteJid!
if (!this.messages.has(jid)) {
this.messages.set(jid, [])
}
this.messages.get(jid)!.push(msg)
}
}
getMessage = async (key: WAMessageKey): Promise<WAMessage | undefined> => {
const messages = this.messages.get(key.remoteJid!)
return messages?.find(m => m.key.id === key.id)
}
}
const sock = makeWASocket({
getMessage: async (key) => await messageStore.getMessage(key)
})
const messageStore = new MessageStore(sock)