Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/abdelhafid37/talkbox/llms.txt

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

TalkBox delivers messages in real time using Socket.IO. When a user sends a message, the client emits a socket event; the server persists the document to MongoDB, populates the sender and receiver usernames, and immediately pushes the fully-formed message object back to both participants. Conversation history is loaded separately via a standard REST endpoint the first time a conversation is opened.

How it works

Socket.IO

Handles live, bidirectional message delivery. Every new message is emitted to both the sender and receiver via their individual socket connections.

REST API

Used for loading existing conversation history when a chat is opened. Returns messages sorted chronologically from MongoDB.

Connection setup

The client creates a single shared socket instance with autoConnect: false, which prevents it from connecting immediately when the module is first imported:
import { io } from "socket.io-client";

const socket = io(import.meta.env.VITE_API_URL, {
  autoConnect: false,
});

export default socket;
The socket is connected explicitly inside AuthProvider once both a valid token and a loaded user object are available. Immediately after the connect event fires, the client emits a "join" event to register itself on the server:
socket.auth = { token };
socket.connect();

socket.on("connect", () => {
  console.log("Socket Connected:", socket.id);
  socket.emit("join");
});

Sending a message

When the user submits the message form in ChatPage, the client emits a "sendMessage" event with the recipient’s ID and the message text:
function handleSubmit(event) {
  event.preventDefault();

  if (!selectedUser || !message.trim()) return;

  socket.emit("sendMessage", {
    receiverId: selectedUser._id,
    text: message,
  });

  setMessage("");
  inputRef.current?.focus();
}
On the server, the "sendMessage" handler creates a Message document in MongoDB, populates the sender.username and receiver.username fields, then emits the populated message to both the sender’s and receiver’s sockets:
socket.on("sendMessage", async (data) => {
  try {
    const message = await Message.create({
      sender: socket.user._id,
      receiver: data.receiverId,
      content: data.text,
    });

    const populatedMessage = await message.populate([
      { path: "sender", select: "username" },
      { path: "receiver", select: "username" },
    ]);

    const receiverSocketId = onlineUsers.get(data.receiverId);

    socket.emit("newMessage", populatedMessage);
    io.to(receiverSocketId).emit("newMessage", populatedMessage);
  } catch (error) {
    console.error(`[SOCKET.IO] [MESSAGE] ${error.message}`);
  }
});
The sender identity is taken from socket.user — the authenticated user object attached during the Socket.IO middleware handshake — so clients cannot spoof who sent a message.

Receiving messages

ChatPage listens for the "newMessage" event for the lifetime of a selected conversation. Before appending, it verifies that the incoming message belongs to the currently open conversation so messages from other threads are not mixed in:
useEffect(() => {
  socket.on("newMessage", (message) => {
    if (!selectedUser) return;

    const isFromSelectedUser = message.sender._id === selectedUser._id;
    const isToSelectedUser = message.receiver._id === selectedUser._id;

    const isFromMe = message.sender._id === user._id;
    const isToMe = message.receiver._id === user._id;

    const belongsToConversation =
      (isFromSelectedUser && isToMe) || (isToSelectedUser && isFromMe);

    if (!belongsToConversation) return;

    setMessages((prev) => [...prev, message]);
  });

  return () => socket.off("newMessage");
}, [selectedUser, user]);
The effect cleans up its listener when selectedUser or user changes, ensuring no stale closures accumulate.
Messages are persisted to MongoDB regardless of whether the recipient is currently connected. Socket.IO delivery only works when the recipient has an active socket connection. If they are offline at the time the message is sent, they will see it the next time they open that conversation via the REST history endpoint.

Loading conversation history

When the user selects a contact in the sidebar, ChatPage calls the REST endpoint to fetch the full message history for that pair of users. The endpoint returns messages sorted ascending by createdAt:
useEffect(() => {
  if (!selectedUser) return;

  async function fetchConversation() {
    setMessages([]);
    setIsMessagesLoading(true);

    try {
      const conversation = await getConversation(selectedUser._id);
      setMessages(conversation);
    } finally {
      setIsMessagesLoading(false);
    }
  }

  fetchConversation();
}, [selectedUser]);
Endpoint: GET /api/messages/:userId The messageService helper attaches the JWT token from localStorage:
async function getConversation(userId) {
  const token = localStorage.getItem("token");
  const response = await api.get(`/messages/${userId}`, {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });

  return response.data;
}
On the server, getConversationController queries MongoDB for all messages where the two users are either the sender or receiver, sorts them by createdAt ascending, and populates the username fields:
const conversation = await Message.find({
  $or: [
    { sender: userId, receiver: otherUserId },
    { sender: otherUserId, receiver: userId },
  ],
})
  .sort({ createdAt: 1 })
  .populate("sender", "username")
  .populate("receiver", "username");

Message data shape

Each message returned by both the socket event and the REST endpoint has the following structure after population:
{
  "_id": "64f1a2b3c4d5e6f7a8b9c0d1",
  "sender": {
    "_id": "64e0000000000000000000aa",
    "username": "alice"
  },
  "receiver": {
    "_id": "64e0000000000000000000bb",
    "username": "bob"
  },
  "content": "Hey, are you around?",
  "createdAt": "2024-01-15T10:30:00.000Z",
  "updatedAt": "2024-01-15T10:30:00.000Z"
}
The createdAt timestamp is an ISO 8601 string and is formatted for display using date-fns in the MessageList component.

Build docs developers (and LLMs) love