Messaging Module
Location: apps/api/src/modules/messaging/
Aggregate roots: Conversation, ConversationMember, Message, MessageReaction
The Messaging context provides DM and small-group conversations. A privacy gate ensures DMs can only be opened between users who share at least one circle. Real-time delivery is planned via Centrifugo; clients currently poll.
Prisma Models
| Model | Table | Notes |
|---|---|---|
| Conversation | conversations | DM or group (kind='dm'|'group') |
| ConversationMember | conversation_members | Member row with role + lastReadAt |
| Message | messages | Body text, append-only |
| MessageReaction | message_reactions | Emoji reaction per (message, user, emoji) |
Conversation Fields
| Field | Type | Notes |
|---|---|---|
| id | UUID PK | |
| kind | String | dm | group |
| title | String? | Group title |
| createdAt | DateTime | |
| updatedAt | DateTime |
ConversationMember Fields
| Field | Type | Notes |
|---|---|---|
| conversationId | UUID FK | |
| userId | UUID FK | |
| role | String | owner | member |
| joinedAt | DateTime | |
| lastReadAt | DateTime? | Tracks read receipts |
| mutedAt | DateTime? | Per-conversation mute |
Message Fields
| Field | Type | Notes |
|---|---|---|
| id | UUID PK | |
| conversationId | UUID FK | |
| authorId | UUID FK | |
| body | String | Max 4,000 chars |
| createdAt | DateTime |
MessageReaction Fields
| Field | Type | Notes |
|---|---|---|
| messageId | UUID FK | |
| userId | UUID FK | |
| emoji | String | 1–8 graphemes (server-validated) |
| createdAt | DateTime |
PK: (messageId, userId, emoji)
Key Methods
| Method | Description |
|---|---|
openDm(userId, otherUserId) | Open or reuse a DM; privacy-gated to shared circles |
createGroup(userId, title, memberIds) | Create group conversation (max 50 members) |
sendMessage(conversationId, userId, body) | Post a message; emit messaging.message.posted |
listConversations(userId) | All conversations with last message + unread count |
listMessages(conversationId, userId, opts) | Paginated message history |
markRead(conversationId, userId) | Update lastReadAt |
addMember(conversationId, userId, newMemberId) | Add to group; emits messaging.member.added |
removeMember(conversationId, userId, targetId) | Remove from group |
muteConversation(conversationId, userId, mute) | Toggle per-conversation mute |
addReaction(messageId, userId, emoji) | React to a message |
removeReaction(messageId, userId, emoji) | Remove reaction |
HTTP Endpoints
All under /api/v1/messaging/. Require JwtAuthGuard.
| Method | Path | Description |
|---|---|---|
| GET | /messaging/conversations | List my conversations |
| POST | /messaging/conversations/dm | Open DM { otherUserId } |
| POST | /messaging/conversations/group | Create group { title, memberIds } |
| GET | /messaging/conversations/:id/messages | Message history |
| POST | /messaging/conversations/:id/messages | Send message { body } |
| POST | /messaging/conversations/:id/read | Mark read |
| POST | /messaging/conversations/:id/mute | Mute/unmute { muted: bool } |
| POST | /messaging/conversations/:id/members | Add member (group) |
| DELETE | /messaging/conversations/:id/members/:uid | Remove member |
| POST | /messaging/messages/:id/reactions | Add reaction { emoji } |
| DELETE | /messaging/messages/:id/reactions/:emoji | Remove reaction |
Constraints
| Rule | Value |
|---|---|
| Max group size | 50 members (MAX_GROUP_SIZE) |
| Max message length | 4,000 chars (MAX_MESSAGE_LENGTH) |
| DM privacy gate | Sender and recipient must share at least one circle |
| Muted conversations | No push/in-app notifications when mutedAt is set |
Events Emitted
| Event | Payload | Consumed by |
|---|---|---|
messaging.conversation.created | { conversationId, kind, creatorId } | (observability) |
messaging.message.posted | { messageId, conversationId, authorId } | Notifications |
messaging.member.added | { conversationId, memberId, addedBy } | Notifications |