Message Group

implemented · REST first
Module: src/modules/message-group  ·  Mount: /message-group  ·  Realtime: not included yet
3
Routes
create · send · list messages
2
Group Types
PRIVATE · CROWD
2
Public IDs
group.publicId · message.publicId
REST
Delivery
Pusher phase sau

Implemented Flow


Clientauthorized user MessageGroupControllerZod DTO + req.user.sub Service + ReposPrisma transaction CREATE GROUP POST /message-group{ type, members: username[] } Normalize DTOprivate → PRIVATE Find users by usernamemissing username ignored Count valid?PRIVATE=2 · CROWD>=3 no400 BadRequestmember count invalid yesTransactionMessageGroup + MemberMessageGroup[] SEND + LIST MESSAGES POST /:publicId/messagescontent max 1000 Find group + assert member404 group · 403 non-member Create MessagesenderId + content Update lastMessageAtnow() GET /:publicId/messagesafter = messagePublicId · createdAt desccheck cursor belongs to group

Flow hiện tại là REST: tạo group, gửi message, lấy message history. Realtime/Pusher sẽ được triển khai trong phase notification hoặc chat realtime sau.

Current API Contract


#EndpointBehaviorStatus
01
POST /message-group
Create group
Body { type: "private" | "crowd", members: string[] }. Server tự thêm user đăng nhập, tìm user theo username, bỏ qua username không tồn tại.done
02
POST /message-group/:publicId/messages
Send message
Body { content }. Content trim, không rỗng, tối đa 1000 ký tự. Tạo Message và update MessageGroup.lastMessageAt.done
03
GET /message-group/:publicId/messages
List messages
Query after=messagePublicId, take. Nếu cursor không thuộc group thì 404. Sort createdAt desc.done

Schema Snapshot


MessageGroup
id, publicId, groupType, createdById, lastMessageAt. Client chỉ dùng publicId.
MemberMessageGroup
Liên kết user với group, có unique messageGroupId + userId để tránh duplicate member.
Message
publicId làm cursor, messageGroupId, senderId, content, createdAt. Pagination dùng cursor publicId.

Next Phase


Realtime
Dùng Pusher để trigger message:new tới private channel của từng user hoặc từng message group.
Inbox list
Cần route list message groups của user, sort theo lastMessageAt desc.
Read state
Nếu cần seen/unread thì bổ sung vào MemberMessageGroup: lastSeenMessageId, unreadCount.

Chat 1-1 Realtime

chat module · websocket
Công nghệ: Pusher (Soketi) hoặc Socket.IO  ·  DB: MessageGroup + MemberMessageGroup (có sẵn Prisma)
Stub
Service
sendMessage() → return {}
2
Routes
POST /message · PATCH /seen
0
Bảng Message
chưa có — cần tạo
Pusher
Realtime
Soketi đã setup

Proposed: 1-1 Message Flow


User A (sender) User B (receiver) POST /chat/message Server: validate auth + content + receiverId Conversation? chưa có Create Conversation MessageGroup + 2 MemberMessageGroup có rồi INSERT Message conversationId, senderId, content, status=SENT Pusher: trigger event channel=chat-{conversationId}, event=new-message Nhận tin nhắn realtime Response: message saved PATCH /message/:id/seen status → SEEN, seenAt = now() Pusher: trigger message-seen User A thấy tick "đã xem" ✓✓ DB operation realtime event receiver side

Flow hoàn chỉnh: gửi → lưu DB → broadcast qua Pusher → receiver nhận realtime → đánh dấu "đã xem" → sender thấy tick ✓✓

Proposed: Database Schema


MessageGroup
Đã có. Đại diện cho 1 conversation. Thêm fields: lastMessageAt, lastMessagePreview để sort danh sách chat.
MemberMsgGroup
Đã có. Liên kết user ↔ conversation. Thêm: unreadCount, lastSeenMessageId.
Message ⛔
Cần tạo mới: Message { id, conversationId, senderId, content, type (TEXT/IMAGE/FILE), status (SENT/DELIVERED/SEEN), seenAt, createdAt }

Implementation Checklist


#TaskChi tiếtStatus
01
Prisma: model Message
Tạo bảng Message với indexes trên conversationId + createdAt. FK tới MessageGroupUser.todo
02
POST /chat/message
Gửi tin nhắn
Validate → find/create conversation → INSERT message → Pusher trigger new-message event → response.stub
03
PATCH /:messageId/seen
Đánh dấu đã xem
Update status=SEEN, seenAt=now(). Pusher trigger message-seen cho sender. Cập nhật unreadCount.stub
04
GET /chat/conversations
Danh sách cuộc hội thoại
Lấy danh sách conversations của user. Sort theo lastMessageAt DESC. Include: otherUser info, lastMessage preview, unreadCount.todo
05
GET /chat/:conversationId/messages
Lịch sử tin nhắn
Cursor pagination (cũ → mới). Load 20 tin gần nhất, scroll lên load thêm. Trả kèm isMine boolean.todo
06
Pusher channel auth
Private channel
Channel private-chat-{conversationId}. Auth endpoint verify user là member. Chỉ 2 user trong conversation nhận event.todo
07
Typing indicator
Client event
Pusher client event client-typing. Không qua server, chỉ broadcast giữa 2 client. FE hiện "đang nhập..."todo
08
Online status
Presence channel
Pusher presence channel presence-user-{userId}. FE biết user nào đang online (chấm xanh).todo

Potential Issues


Message order
Tin nhắn gửi đồng thời có thể đến không đúng thứ tự. Giải pháp: server timestamp + client sort theo createdAt. Optimistic UI hiện tin ngay, reorder khi server confirm.
Offline
User B offline khi A gửi tin. Cần: 1) Lưu DB trước, 2) Pusher deliver khi B online, 3) Khi B mở app → load unread từ API. Push notification (FCM/APNs) cho mobile.
Scale
Pusher (Soketi) có limit connections. Nếu >10K concurrent cần horizontal scale Soketi hoặc chuyển sang Socket.IO cluster (Redis adapter).
File/Image
Gửi ảnh/file: upload qua Upload module trước (giống Post media), nhận URL, gửi message type=IMAGE/FILE. Không gửi base64 qua WebSocket.
Encryption
E2E encryption (tùy chọn): nếu cần, dùng Signal Protocol. Phức tạp, khuyến nghị Phase 3+. Phase 1 dùng TLS đủ an toàn.

Tech Stack


LayerCông nghệGhi chú
RealtimePusher (Soketi self-hosted)Đã setup. Private channels cho auth. Client events cho typing. Presence cho online status.
DatabaseMySQL (Prisma)Bảng Message + MessageGroup. Index on (conversationId, createdAt) cho query nhanh.
CachingRedisCache danh sách conversation, unread count. Invalidate khi có message mới.
File uploadHLS / S3 / Upload moduleReuse upload infrastructure sẵn có. Message chỉ lưu URL reference.