Back-EndSchemas
Chat Schema
Complete MongoDB schemas for a real-time chat application — Conversation and Message.
Chat Schema
Schemas for a chat app — supports both one-on-one and group conversations through a single flexible Conversation model, with messages referencing their conversation.
Conversation Schema
// src/models/conversation.model.js
import mongoose from "mongoose";
const conversationSchema = new mongoose.Schema(
{
participants: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
],
isGroup: {
type: Boolean,
default: false,
},
// only used when isGroup is true
groupName: {
type: String,
trim: true,
},
groupAdmin: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
},
lastMessage: {
type: mongoose.Schema.Types.ObjectId,
ref: "Message",
},
},
{ timestamps: true }
);
const Conversation = mongoose.model("Conversation", conversationSchema);
export default Conversation;Message Schema
// src/models/message.model.js
import mongoose from "mongoose";
const messageSchema = new mongoose.Schema(
{
conversation: {
type: mongoose.Schema.Types.ObjectId,
ref: "Conversation",
required: true,
},
sender: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
content: {
type: String,
trim: true,
},
// optional file/image attachment
attachment: {
url: { type: String },
type: { type: String, enum: ["image", "video", "file"] },
},
// tracks which participants have read this message
readBy: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }],
},
{ timestamps: true }
);
const Message = mongoose.model("Message", messageSchema);
export default Message;Finding or Creating a One-on-One Conversation
// find an existing 1-on-1 conversation between two users, or create one
async function findOrCreateConversation(userIdA, userIdB) {
let conversation = await Conversation.findOne({
isGroup: false,
participants: { $all: [userIdA, userIdB], $size: 2 },
});
if (!conversation) {
conversation = await Conversation.create({
participants: [userIdA, userIdB],
isGroup: false,
});
}
return conversation;
}Sending a Message
async function sendMessage(conversationId, senderId, content) {
// create the message
const message = await Message.create({
conversation: conversationId,
sender: senderId,
content,
readBy: [senderId], // sender has implicitly "read" their own message
});
// update the conversation's lastMessage pointer for inbox previews
await Conversation.findByIdAndUpdate(conversationId, {
lastMessage: message._id,
});
return message;
}Fetching a User's Conversation List
const conversations = await Conversation.find({
participants: userId,
})
.populate("participants", "name avatar")
.populate("lastMessage")
.sort({ updatedAt: -1 }); // most recently active conversations firstlastMessage is stored as a reference on Conversation and updated every time a new message is sent. This avoids running a separate query to find each conversation's most recent message when rendering an inbox list.
Summary
- A single
Conversationmodel handles both one-on-one and group chats via theisGroupflag —participantsis just a longer array for groups $allcombined with$size: 2is the standard pattern for finding an existing 1-on-1 conversation between exactly two usersMessage.readBytracks read receipts per participantConversation.lastMessageis denormalized and updated on every new message — makes rendering an inbox list fast without extra queries- Pair this schema with the Socket.io Setup file for real-time delivery