Chat Message Model and Database Design MongoDB Schema for Chat
Learn how to design the MongoDB schema for a real-time chat feature message model, indexes for performance, and conversation management.
Chat Message Model and Database Design
The Message model is the foundation of your chat feature. This guide covers schema design and indexes for optimal performance.
Message Schema
const mongoose = require("mongoose"); const messageSchema = new mongoose.Schema({ senderId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true, index: true }, receiverId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true, index: true }, text: { type: String, required: true, maxLength: 1000 }, read: { type: Boolean, default: false }, readAt: { type: Date, default: null } }, { timestamps: true // Adds createdAt and updatedAt }); // Compound index for efficient message history queries messageSchema.index({ senderId: 1, receiverId: 1, createdAt: -1 }); // Index for unread message count queries messageSchema.index({ receiverId: 1, read: 1 }); const Message = mongoose.model("Message", messageSchema); module.exports = Message;
Why These Indexes?
Compound index (senderId, receiverId, createdAt):
- Speeds up loading message history between two users
- Sorts by createdAt descending (newest first)
- Used in:
Message.find({ senderId, receiverId }).sort({ createdAt: -1 })
Receiver + read index:
- Speeds up finding unread messages
- Used in:
Message.countDocuments({ receiverId: userId, read: false })
Conversation Model (Optional)
If you want to track conversations (chat list):
const conversationSchema = new mongoose.Schema({ participants: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }], lastMessage: { type: mongoose.Schema.Types.ObjectId, ref: "Message" }, lastMessageAt: { type: Date, default: Date.now }, unreadCount: { type: Map, of: Number, default: {} } }, { timestamps: true }); conversationSchema.index({ participants: 1 }); conversationSchema.index({ lastMessageAt: -1 }); const Conversation = mongoose.model("Conversation", conversationSchema);
The Conversation model stores:
- participants The two users in the conversation
- lastMessage Reference to the most recent message
- lastMessageAt For sorting the chat list
- unreadCount Map of userId → unread count
Creating a Message
const createMessage = async (senderId, receiverId, text) => { const message = await Message.create({ senderId, receiverId, text }); // Update or create conversation await Conversation.findOneAndUpdate( { participants: { $all: [senderId, receiverId] } }, { $set: { lastMessage: message._id, lastMessageAt: new Date() }, $inc: { [`unreadCount.${receiverId}`]: 1 } }, { upsert: true } ); return message; };
Loading Message History
const getMessageHistory = async (userId1, userId2, page = 1, limit = 50) => { const skip = (page - 1) * limit; const messages = await Message.find({ $or: [ { senderId: userId1, receiverId: userId2 }, { senderId: userId2, receiverId: userId1 } ] }) .sort({ createdAt: -1 }) .skip(skip) .limit(limit) .populate("senderId", "firstName lastName photoUrl"); return messages.reverse(); // Reverse to show oldest first };
Marking Messages as Read
const markAsRead = async (receiverId, senderId) => { await Message.updateMany( { senderId: senderId, receiverId: receiverId, read: false }, { $set: { read: true, readAt: new Date() } } ); // Reset unread count in conversation await Conversation.findOneAndUpdate( { participants: { $all: [receiverId, senderId] } }, { $set: { [`unreadCount.${receiverId}`]: 0 } } ); };
Getting Chat List
const getChatList = async (userId) => { const conversations = await Conversation.find({ participants: userId }) .sort({ lastMessageAt: -1 }) .populate("participants", "firstName lastName photoUrl") .populate("lastMessage"); return conversations.map((conv) => ({ _id: conv._id, otherUser: conv.participants.find( (p) => p._id.toString() !== userId.toString() ), lastMessage: conv.lastMessage, lastMessageAt: conv.lastMessageAt, unreadCount: conv.unreadCount.get(userId.toString()) || 0 })); };
The Takeaway
The chat database design includes a Message model (senderId, receiverId, text, read, readAt) with compound indexes for efficient queries, and optionally a Conversation model (participants, lastMessage, unreadCount) for the chat list. Use compound index on (senderId, receiverId, createdAt) for message history, and (receiverId, read) for unread counts. Update conversation's lastMessage and unreadCount when a new message is created.
senderId (ObjectId ref User), receiverId (ObjectId ref User), text (String, maxLength 1000), read (Boolean, default false), readAt (Date), and timestamps (createdAt, updatedAt from Mongoose schema options).
Compound index on (senderId, receiverId, createdAt) for efficient message history queries with sorting. Index on (receiverId, read) for finding unread messages. Index on senderId and receiverId individually for user-specific queries.
Yes, if you need a chat list. The Conversation model stores participants (two user IDs), lastMessage (reference to Message), lastMessageAt (for sorting), and unreadCount (Map of userId → count). Update it when a new message is created and when messages are marked as read.
Query Message with $or for both directions (senderId=A receiverId=B OR senderId=B receiverId=A), sort by createdAt -1 (newest first), skip (page-1)*limit, limit. Reverse the result to show oldest first. Use the compound index for efficiency.
Add a read boolean to the Message model. When a user opens a chat, update all unread messages to read: true. Store unreadCount per user in the Conversation model as a Map (userId → count). Increment on new message, reset to 0 when messages are read.
Ready to master Node.js completely?
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.
Master Node.js
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course.

