A scalable, production-ready chat backend built with Node.js, Express, TypeScript, and MongoDB, designed using clean architecture principles and industry-standard messaging patterns.
This README documents all the work completed so far, the design decisions, and flow diagrams to clearly explain how the system works.
✅ JWT-based Authentication (protected routes)
✅ Conversations
- 1:1 (DM) conversations
- Group conversations
- Group admins support
✅ Messages
- Send messages
- Fetch messages with pagination
- Conversation-level lastMessage tracking
✅ Unread Message System (Industry Standard)
- Per-user read tracking
- Unread count per conversation
- Works for DM and Group chats
- Pagination-safe
Stores basic user details (name, avatar, etc.).
Represents a chat (DM or Group).
Key Fields:
type:dm | groupparticipants: User IDsadmins: User IDs (group only)lastMessage: Message ID
Represents a single chat message.
Key Fields:
conversationIdsendercontenttype(text / image / file)createdAt
Messages are immutable.
Tracks read state per user per conversation.
Meaning:
For a given user and conversation, what is the last message the user has read?
Key Fields:
conversationIduserIdlastReadMessageId
This model enables scalable unread message logic.
❌ Messages are NOT individually marked as read
✅ Each user maintains a read pointer (bookmark) per conversation
A message is considered read if:
message._id <= lastReadMessageId
Unread messages are derived, not stored.
User
│
▼
Auth Middleware
│
▼
sendMessageService
│
├─ Validate conversation
├─ Validate participant
│
▼
Create Message
│
▼
Update Conversation.lastMessage
│
▼
Return message
📌 Note:
- No unread state updated here
- Message writes stay fast
User opens chat
│
▼
GET /messages/:conversationId
│
▼
fetchMessagesService
│
├─ Validate conversation & participant
│
▼
Fetch messages (paginated)
│
▼
Find ConversationRead
│ ├─ If not exists → create
│
▼
Update lastReadMessageId (latest message)
│
▼
Return messages
📌 This is the only place where messages are marked as read.
User opens chat list
│
▼
GET /conversations
│
▼
getConversationsService
│
▼
Fetch all user conversations
│
▼
For each conversation:
│
├─ Get ConversationRead
├─ Build unread query
│ ├─ Same conversation
│ ├─ Sender != user
│ └─ Message > lastReadMessageId
│
▼
Count unread messages
│
▼
Attach unreadCount
│
▼
Return conversation list
User
│
▼
Conversation
│
▼
Message
▲
│
ConversationRead
Conversationis sharedMessageis immutableConversationReadis user-specific
Unread Messages =
Messages where:
- conversationId matches
- sender != current user
- message._id > lastReadMessageId
If lastReadMessageId is null → all messages from others are unread.
A sends m1
B sends m2
B sends m3
A opens chat → lastRead = m3
B sends m4
Unread count for A:
1 (m4)
Unread count for B:
0
- REST APIs create data
- Socket.IO delivers events
- Clients never send messages via socket
- Server emits socket events after DB persistence
conversation:<conversationId>
Used for:
- Real-time messages
- Typing indicators (future)
- Read receipts (future)
Joined when:
- User opens a conversation
Left when:
- User switches conversation
Used for:
- Notifications
- Unread updates
- Presence (future)
Joined on:
- Socket connection
Send message → REST API
→ DB save
→ REST response
→ Sender UI updates immediately
socket.on("message:new")
→ append message to state
→ UI updates instantly
- Pagination-safe unread logic
- No per-message read flags
- One DB write per chat open
- Works for DM & Group chats
- Real-time messaging using Socket.IO
- Real-time unread updates
- Aggregation-based unread optimization
- Read receipts ("Seen by X")
- Group admin permissions
This backend implements an industry-grade chat architecture similar to WhatsApp / Slack:
- Clean separation of concerns
- Scalable unread message design
- With real-time features
Frontend does not access refresh tokens directly.
- Socket.IO real-time messaging
- Redis-backed unread optimization
- Read receipts (“Seen by”)
- Group permission enforcement
Zolo implements an industry-grade chat architecture with:
👨💻 Built as part of the Zolo Project