Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/khushboodaryani/Chat-App/llms.txt

Use this file to discover all available pages before exploring further.

Chat App persists all data in MongoDB through three Mongoose models: User, Message, and Conversation. Users are the root entity — every message and every conversation references User documents by their _id. Messages store both sender and receiver directly so they can be resolved independently. Conversations act as the join record that groups a pair of users with their shared message history, and Mongoose’s populate method is used to hydrate those message IDs into full documents at read time.

Connecting to MongoDB

The database connection is established in backend/db/connectToMongoDB.js using Mongoose’s connect method. The connection string is read from the MONGO_URI environment variable.
import mongoose from "mongoose";

const connectToMongoDB = async () => {
  try {
    await mongoose.connect(process.env.MONGO_URI);
    console.log("Connected to MongoDB");
  } catch (error) {
    console.log("Error connecting to MongoDB", error.message);
  }
};

export default connectToMongoDB;
connectToMongoDB is called inside the server.listen callback so the server only accepts traffic after the database connection is confirmed.

User Schema

Each document in the users collection represents one registered account. Passwords are hashed with bcryptjs before the document is saved — the plain-text password never reaches MongoDB.
const userSchema = new mongoose.Schema(
  {
    fullName:   { type: String, required: true },
    username:   { type: String, required: true, unique: true },
    password:   { type: String, required: true, minlength: 6 },
    gender:     { type: String, required: true, enum: ["male", "female"] },
    profilePic: { type: String, default: "" },
  },
  { timestamps: true }
);
FieldTypeConstraintsNotes
fullNameStringRequiredDisplay name shown in the UI
usernameStringRequired, uniqueUsed for login; must be globally unique
passwordStringRequired, minlength 6Stored as a bcryptjs hash
genderStringRequired, enum ["male","female"]Used to assign a default avatar URL
profilePicStringDefault ""URL of the user’s avatar image
createdAtDateAuto (timestamps)Displayed as “Member since …” in the UI
updatedAtDateAuto (timestamps)Managed by Mongoose
The password field is never returned in API responses. getUsersForSidebar appends .select("-password") to its query, and the protectRoute middleware also excludes it when attaching req.user. This is enforced at the database query layer, not in a serialisation helper.

Message Schema

Each document in the messages collection represents a single chat message. Both the sender and the receiver are stored as direct references, which means a message can be resolved without traversing the Conversation collection.
const messageSchema = new mongoose.Schema(
  {
    senderId:   { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
    receiverId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
    message:    { type: String, required: true },
  },
  { timestamps: true }
);
FieldTypeConstraintsNotes
senderIdObjectId → UserRequiredThe user who wrote the message
receiverIdObjectId → UserRequiredThe intended recipient
messageStringRequiredThe plain-text message body
createdAtDateAuto (timestamps)Used to render the message timestamp
updatedAtDateAuto (timestamps)Managed by Mongoose

Conversation Schema

Each document in the conversations collection represents the persistent channel between exactly two users. It holds a participants array of two User IDs and a messages array of Message IDs that grows as the conversation progresses.
const conversationSchema = new mongoose.Schema(
  {
    participants: [{ type: mongoose.Schema.Types.ObjectId, ref: "User" }],
    messages:     [{ type: mongoose.Schema.Types.ObjectId, ref: "Message", default: [] }],
  },
  { timestamps: true }
);
FieldTypeNotes
participants[ObjectId → User]Always two entries — the two users in the conversation
messages[ObjectId → Message]Ordered array of message IDs; defaults to [] on creation
createdAtDateAuto (timestamps)
updatedAtDateAuto (timestamps)

Query Pattern: Finding a Shared Conversation

To fetch or create the conversation between two specific users, the message controller uses MongoDB’s $all operator to match conversations where both IDs appear in participants, regardless of their order in the array:
let conversation = await Conversation.findOne({
  participants: { $all: [senderId, receiverId] },
});
If no conversation exists yet (first message ever sent between two users), a new one is created and the sender and receiver are added to participants.

Populating Messages

When loading the message history for an open conversation, .populate("messages") replaces each ObjectId in the messages array with the full Message document:
const conversation = await Conversation.findOne({
  participants: { $all: [senderId, receiverId] },
}).populate("messages");

// conversation.messages is now an array of Message documents, not IDs
This single query is sufficient to render the entire chat history — no secondary lookup is needed.

Relationship Diagram

The three collections relate to each other as follows:
  • A Conversation contains exactly two User references in participants and an ordered array of Message references in messages.
  • A Message stores its senderId and receiverId as direct User references, independent of the Conversation. This allows the Socket.io layer to emit a new message to the recipient immediately after saving, without re-querying the Conversation document.
  • A User has no embedded references to conversations or messages — all relationships are owned by the Conversation and Message documents, keeping the User schema lean.
User ◄────────────── Message
  ▲   senderId / receiverId

  └── participants

     Conversation

         └── messages ──► Message
Because Message stores senderId and receiverId directly, the real-time delivery path (saving a message → emitting via Socket.io) never needs to touch the Conversation collection. Only the read path (getMessages) populates the conversation to return history.

Build docs developers (and LLMs) love