DocsHub
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 first

lastMessage 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 Conversation model handles both one-on-one and group chats via the isGroup flag — participants is just a longer array for groups
  • $all combined with $size: 2 is the standard pattern for finding an existing 1-on-1 conversation between exactly two users
  • Message.readBy tracks read receipts per participant
  • Conversation.lastMessage is 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

On this page