Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/DerBasilisk/SEA-ServicioEvaluaconAsistida/llms.txt

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

Sealearn’s chat system pairs a set of REST endpoints for managing conversations and paginating message history with a Socket.IO namespace for real-time delivery. The REST layer handles the data model — creating rooms, fetching history, tracking unread counts — while the /chat Socket.IO namespace handles live events like typing indicators, read receipts, and message broadcasting. Both layers share the same JWT authentication mechanism.
REST routes are mounted under /api/chat and protected by verificarToken. The Socket.IO namespace is /chat — make sure to specify it when connecting. Images must be uploaded via the REST upload endpoint before being sent through the socket.

Data Models

Conversation

A Conversation document represents either a 1-on-1 direct chat ("direct") or a named group ("group"). The server enforces uniqueness on direct conversations so that two users always share exactly one direct conversation regardless of how many times the client calls openDirect.
FieldTypeDescription
_idObjectIdUnique conversation ID.
type"direct" | "group"Chat type.
namestring | nullGroup name (max 50 characters); null for direct conversations.
avatarstring | nullGroup avatar URL; null for direct conversations.
participantsObjectId[] → UserAll users who are members of this conversation.
createdByObjectId → User | nullUser who created the group; null for direct conversations.
lastMessageObjectId → Message | nullReference to the most recent message, used for conversation list previews.
lastActivityDateUpdated whenever a new message is sent; used for sorting the conversation list.
lastDuelObjectId → Duel | nullReference to the most recent duel associated with this conversation.
createdAtDateMongoose auto-timestamp.
updatedAtDateMongoose auto-timestamp.

Message

A Message document records a single message within a conversation. Messages are never hard-deleted — a deletedAt timestamp is set instead (soft delete). The type field controls how the client renders the message bubble.
FieldTypeDescription
_idObjectIdUnique message ID.
conversationObjectId → ConversationParent conversation.
senderObjectId → UserUser who sent the message.
type"text" | "image" | "duel_invite" | "duel_result"Rendering hint for the client.
contentstringText content or Cloudinary image URL (max 2 000 characters).
duelDataobject | nullPresent on duel_invite and duel_result messages; contains duelId, resultSummary, inviteCode, and expiresAt.
readByObjectId[] → UserIDs of users who have read the message.
deletedAtDate | nullSet on soft-delete; null while the message is visible.
editedbooleantrue if the sender has edited the content after sending.
createdAtDateMongoose auto-timestamp.

REST Endpoints

GET /api/chat/conversations

Returns all conversations the authenticated user belongs to, sorted by lastActivity descending.
ok
boolean
required
true on success.
conversations
array
required
Array of populated Conversation documents, each with its lastMessage and participants populated.
curl -X GET https://api.sealearn.app/api/chat/conversations \
  -H "Authorization: Bearer <token>"
{
  "ok": true,
  "conversations": [
    {
      "_id": "665c1a2b3d4e5f006a7b8c90",
      "type": "direct",
      "name": null,
      "participants": [
        { "_id": "664a0011ab12cd003e7f1122", "username": "codigoninja", "displayName": "Código Ninja", "avatar": "..." },
        { "_id": "664a1f2e8b3c2a001e4d8e10", "username": "marisolita",  "displayName": "Marisol",      "avatar": "..." }
      ],
      "lastMessage": {
        "_id": "665c1a2b3d4e5f006a7b8c91",
        "content": "¡Vamos por la lección 5!",
        "type": "text",
        "sender": "664a0011ab12cd003e7f1122",
        "createdAt": "2024-06-01T10:15:00.000Z"
      },
      "lastActivity": "2024-06-01T10:15:00.000Z"
    }
  ]
}

POST /api/chat/conversations/direct

Gets an existing direct conversation between the authenticated user and targetUserId, or creates one if it does not yet exist. Safe to call multiple times — idempotent.
targetUserId
string
required
MongoDB _id of the other user. Cannot be the same as the authenticated user’s own ID.
ok
boolean
required
true on success.
conversation
object
required
The direct Conversation document (existing or newly created).
curl -X POST https://api.sealearn.app/api/chat/conversations/direct \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "targetUserId": "664a1f2e8b3c2a001e4d8e10" }'
{
  "ok": true,
  "conversation": {
    "_id": "665c1a2b3d4e5f006a7b8c90",
    "type": "direct",
    "participants": ["664a0011ab12cd003e7f1122", "664a1f2e8b3c2a001e4d8e10"],
    "lastActivity": "2024-06-01T10:15:00.000Z"
  }
}

POST /api/chat/conversations/group

Creates a new group conversation. The authenticated user is automatically added to participants as the creator.
name
string
required
Group name. Must be a non-empty string, maximum 50 characters.
participantIds
array
required
Array of user _id strings to include in the group. Must contain at least one entry (in addition to the creator).
ok
boolean
required
true on success (201 Created).
conversation
object
required
The newly created group Conversation document.
curl -X POST https://api.sealearn.app/api/chat/conversations/group \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Equipo Física",
    "participantIds": ["664a1f2e8b3c2a001e4d8e10", "664a0011ab12cd003e7f1122"]
  }'
{
  "ok": true,
  "conversation": {
    "_id": "665d2f3e4a5b6c007d8e9f01",
    "type": "group",
    "name": "Equipo Física",
    "participants": [
      "664a0011ab12cd003e7f1122",
      "664a1f2e8b3c2a001e4d8e10",
      "664b3e1c9f0e4b002f5a9d33"
    ],
    "createdBy": "664a0011ab12cd003e7f1122",
    "lastActivity": "2024-06-01T11:00:00.000Z"
  }
}

POST /api/chat/conversations/:id/participants

Adds a new user to an existing group conversation.
id
string
required
The _id of the group Conversation.
userId
string
required
MongoDB _id of the user to add.
ok
boolean
required
true on success.
conversation
object
required
Updated Conversation document.
curl -X POST https://api.sealearn.app/api/chat/conversations/665d2f3e4a5b6c007d8e9f01/participants \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "userId": "664c9900de11ef004a5b6c22" }'

DELETE /api/chat/conversations/:id/participants/me

Removes the authenticated user from a group conversation. If the group becomes empty after the user leaves, it is deleted entirely.
id
string
required
The _id of the group Conversation to leave.
ok
boolean
required
true on success.
deleted
boolean
required
true if the conversation was deleted because it became empty.
curl -X DELETE https://api.sealearn.app/api/chat/conversations/665d2f3e4a5b6c007d8e9f01/participants/me \
  -H "Authorization: Bearer <token>"
{ "ok": true, "deleted": false }

GET /api/chat/conversations/:id/messages

Fetches the message history for a conversation with cursor-based pagination. Returns messages sorted newest-first. Pass the before query parameter to load earlier pages.
id
string
required
The _id of the Conversation.
before
string
Message _id cursor. When provided, only messages created before the referenced message are returned, enabling infinite-scroll pagination.
ok
boolean
required
true on success.
messages
array
required
Array of Message documents (newest first), each with sender populated.
curl -X GET "https://api.sealearn.app/api/chat/conversations/665c1a2b3d4e5f006a7b8c90/messages?before=665c1a2b3d4e5f006a7b8c91" \
  -H "Authorization: Bearer <token>"
{
  "ok": true,
  "messages": [
    {
      "_id": "665c1a2b3d4e5f006a7b8c91",
      "type": "text",
      "content": "¡Vamos por la lección 5!",
      "sender": { "_id": "664a0011ab12cd003e7f1122", "username": "codigoninja", "avatar": "..." },
      "readBy": ["664a0011ab12cd003e7f1122", "664a1f2e8b3c2a001e4d8e10"],
      "createdAt": "2024-06-01T10:15:00.000Z"
    }
  ]
}

GET /api/chat/unread

Returns an object mapping each conversation ID to the count of unread messages. Use this to display notification badges without fetching full message history.
ok
boolean
required
true on success.
unread
object
required
A map of { conversationId: unreadCount }. Conversations with no unread messages are omitted.
curl -X GET https://api.sealearn.app/api/chat/unread \
  -H "Authorization: Bearer <token>"
{
  "ok": true,
  "unread": {
    "665c1a2b3d4e5f006a7b8c90": 3,
    "665d2f3e4a5b6c007d8e9f01": 1
  }
}

DELETE /api/chat/messages/:id

Soft-deletes a message. Only the message’s own sender can delete it. The document remains in the database with deletedAt set to the current timestamp; clients should render deleted messages as "Mensaje eliminado".
id
string
required
The _id of the Message to delete.
ok
boolean
required
true on success.
curl -X DELETE https://api.sealearn.app/api/chat/messages/665c1a2b3d4e5f006a7b8c91 \
  -H "Authorization: Bearer <token>"
{ "ok": true }

POST /api/chat/upload

Uploads an image to Cloudinary and returns its URL. After receiving the URL, send it over the socket using the chat:send_image event. Images must be under 5 MB and must be a valid image/* MIME type. The request must use multipart/form-data.
image
file
required
The image file to upload (max 5 MB, any image/* content type).
ok
boolean
required
true on success.
url
string
required
Cloudinary secure_url of the uploaded image.
curl -X POST https://api.sealearn.app/api/chat/upload \
  -H "Authorization: Bearer <token>" \
  -F "image=@/path/to/screenshot.png"
{
  "ok": true,
  "url": "https://res.cloudinary.com/sealearn/image/upload/chat/abc123.webp"
}

Socket.IO — /chat Namespace

The /chat namespace handles all real-time messaging. It shares the same Socket.IO server as the duel system but uses a dedicated namespace to keep event spaces clean.

Authentication

Pass the JWT in the auth object when connecting. The middleware decodes the token and attaches socket.userId for all subsequent events.
import { io } from "socket.io-client";

const socket = io("https://api.sealearn.app/chat", {
  auth: { token: "<jwt>" },
});

Rooms

On connection every socket is automatically joined to user:<userId>. This personal room ensures the user receives messages from conversations that are not currently open on screen (used for unread badge notifications). When the user opens a specific conversation, the client emits chat:join to also subscribe to conv:<conversationId>.

Client → Server Events

chat:join

Open a conversation and mark all its messages as read.
socket.emit("chat:join", { conversationId: "665c1a2b3d4e5f006a7b8c90" });
FieldTypeDescription
conversationIdstringThe _id of the Conversation to join.
Server responds with chat:joined on success, or chat:error on failure.

chat:leave

Stop receiving room-scoped events for a conversation without disconnecting.
socket.emit("chat:leave", { conversationId: "665c1a2b3d4e5f006a7b8c90" });

chat:send

Send a plain-text message to a conversation. Content is trimmed and must not exceed 2 000 characters.
socket.emit("chat:send", {
  conversationId: "665c1a2b3d4e5f006a7b8c90",
  content: "¡Vamos por la lección 5!",
});
FieldTypeDescription
conversationIdstringTarget conversation.
contentstringMessage text (max 2 000 characters).

chat:send_image

Send an image message. Upload the file first with POST /api/chat/upload, then emit the returned URL here.
// 1. Upload via REST
const { url } = await fetch("/api/chat/upload", { method: "POST", body: formData }).then(r => r.json());

// 2. Broadcast via socket
socket.emit("chat:send_image", {
  conversationId: "665c1a2b3d4e5f006a7b8c90",
  imageUrl: url,
});

chat:typing

Broadcast a typing indicator to all other participants in a conversation.
socket.emit("chat:typing", { conversationId: "665c1a2b3d4e5f006a7b8c90", isTyping: true });
FieldTypeDescription
conversationIdstringTarget conversation.
isTypingbooleantrue when the user starts typing; false when they stop.

chat:mark_read

Explicitly mark all unread messages in a conversation as read. Also called automatically by chat:join.
socket.emit("chat:mark_read", { conversationId: "665c1a2b3d4e5f006a7b8c90" });

chat:open_direct

Socket-side shortcut to get or create a direct conversation without a separate REST call. Responds with chat:conversation_ready.
socket.emit("chat:open_direct", { targetUserId: "664a1f2e8b3c2a001e4d8e10" });

chat:edit

Edit the content of a message the authenticated user previously sent.
socket.emit("chat:edit", {
  messageId: "665c1a2b3d4e5f006a7b8c91",
  content: "Texto corregido",
});

Server → Client Events

chat:message

Emitted to all sockets in conv:<conversationId> whenever a new message (text, image, or duel result) is saved.
socket.on("chat:message", ({ conversationId, message }) => {
  console.log(message.content);
});
FieldTypeDescription
conversationIdstringConversation the message belongs to.
message._idstringMessage document ID.
message.typestring"text", "image", "duel_invite", or "duel_result".
message.contentstringMessage text or image URL.
message.senderobjectPopulated sender (_id, username, displayName, avatar).
message.readBystring[]Array of user IDs that have read the message.
message.createdAtstringISO 8601 creation timestamp.
message.duelDataobject | undefinedPresent on duel_result messages; contains duelId and resultSummary.

chat:new_message_notify

Emitted to all sockets in the conversation room except those who are also in the conv: room (i.e. users who have the conversation open). Use this to update unread badge counts in the conversation list sidebar.
socket.on("chat:new_message_notify", ({ conversationId, senderId, preview }) => {
  updateUnreadBadge(conversationId);
});

chat:joined

Confirmation that chat:join succeeded.
socket.on("chat:joined", ({ conversationId }) => {
  console.log("Now in conversation:", conversationId);
});

chat:typing

Broadcasts another user’s typing status within the conversation.
socket.on("chat:typing", ({ conversationId, userId, isTyping }) => {
  showTypingIndicator(userId, isTyping);
});

chat:read

Notifies participants that a specific user has read messages in the conversation.
socket.on("chat:read", ({ conversationId, userId }) => {
  renderReadReceipts(conversationId, userId);
});

chat:read_confirmed

Sent back to the socket that emitted chat:mark_read, confirming how many messages were marked.
socket.on("chat:read_confirmed", ({ conversationId, count }) => {
  clearUnreadBadge(conversationId);
});

chat:message_edited

Emitted to the conversation room when a message is edited.
socket.on("chat:message_edited", ({ conversationId, message }) => {
  updateMessageContent(message._id, message.content);
});

chat:conversation_ready

Response to chat:open_direct — provides the ready conversation object.
socket.on("chat:conversation_ready", ({ conversation }) => {
  openChatUI(conversation);
});

chat:error

Emitted back to the originating socket when any handler encounters an error.
socket.on("chat:error", ({ message }) => {
  console.error("Chat error:", message);
});

Full Client Example

import { io } from "socket.io-client";

const chatSocket = io("https://api.sealearn.app/chat", {
  auth: { token: localStorage.getItem("token") },
});

// Connection confirmed
chatSocket.on("connect", () => {
  console.log("Chat connected:", chatSocket.id);

  // Open a conversation
  chatSocket.emit("chat:join", { conversationId: "665c1a2b3d4e5f006a7b8c90" });
});

// Receive incoming messages
chatSocket.on("chat:message", ({ conversationId, message }) => {
  appendMessageToChatUI(message);
});

// Show typing indicators
chatSocket.on("chat:typing", ({ userId, isTyping }) => {
  if (isTyping) showTypingBubble(userId);
  else hideTypingBubble(userId);
});

// Unread badge notifications for conversations not currently open
chatSocket.on("chat:new_message_notify", ({ conversationId }) => {
  incrementUnreadBadge(conversationId);
});

// Send a text message
function sendMessage(conversationId, text) {
  chatSocket.emit("chat:send", { conversationId, content: text });
}

// Broadcast typing state
let typingTimer;
function onInputChange(conversationId) {
  chatSocket.emit("chat:typing", { conversationId, isTyping: true });
  clearTimeout(typingTimer);
  typingTimer = setTimeout(() => {
    chatSocket.emit("chat:typing", { conversationId, isTyping: false });
  }, 1500);
}

// Handle disconnection
chatSocket.on("disconnect", (reason) => {
  console.warn("Chat disconnected:", reason);
});

Build docs developers (and LLMs) love