Skip to content

Epic 17 — Messaging

Secure direct messaging between talents and producers. Rich text with media attachments. Referenced in spec section 2.4.


BE-MSG-001 — Messaging model and API

  • [x] Implemented

Files:

  • Create: src/models/mongo/Message.ts — Mongoose schema (MongoDB for flexible message payloads)
  • Create: src/models/mongo/Conversation.ts
  • Edit: src/graphql/schema/index.ts — add Conversation, Message types + queries/mutations
  • Create: src/graphql/resolvers/messaging.ts
  • Create: src/services/messaging/index.ts

Schema (MongoDB):

ts
// Conversation
{ _id, participantIds: [userId], lastMessageAt, createdAt }

// Message
{ _id, conversationId, senderId, body: String, mediaUrls: [String],
  readBy: [{ userId, readAt }], createdAt }

GraphQL:

graphql
type Conversation { id: ID! participants: [User!]! lastMessage: Message messages(first: Int, after: String): MessageConnection! unreadCount: Int! }
type Message { id: ID! sender: User! body: String mediaUrls: [String!]! createdAt: DateTime! readByMe: Boolean! }

conversations: [Conversation!]!
conversation(id: ID!): Conversation
sendMessage(conversationId: ID, recipientId: ID, body: String!, mediaUrls: [String!]): Message!
markConversationRead(conversationId: ID!): Boolean!

Notes:

  • sendMessage with recipientId (and no conversationId) creates a new conversation if one doesn't exist between the two users.
  • Outside a job context, sending a DM costs 50 CasTars (ties into BE-CASTARS-003 Direct Message Credit).
  • Paginate messages cursor-based (first/after).

FE-MSG-001 — Messaging UI

  • [x] Implemented

Real-time upgrade (post-MVP): the "future enhancement" WebSocket path is now built. The API runs a graphql-ws server on the same /graphql endpoint (shared executable schema) with an in-memory PubSub. Subscriptions: messageAdded(conversationId), conversationUpdated, typing(conversationId); plus a setTyping mutation. The app uses a graphql-ws client (auth via connectionParams) so threads stream live, the inbox + unread badge update on conversationUpdated, and a "typing…" indicator is shown. Polling remains only as a slow (60s) fallback. Deployment note: the reverse proxy must allow WebSocket upgrades on /graphql. To horizontally scale the API, swap the in-memory PubSub for a Redis-backed one (ioredis client already present) — topic names + call sites are unchanged.

Files:

  • Create: apps/app/src/pages/messaging/MessagesPage.tsx — conversation list
  • Create: apps/app/src/pages/messaging/ConversationPage.tsx — message thread
  • Create: apps/app/src/hooks/useMessaging.ts
  • Edit: apps/app/src/App.tsx — add /messages and /messages/:conversationId routes

Description:

  • Conversation list: sorted by lastMessageAt DESC. Each row: avatar, name, last message preview, unread count badge. EmptyState when no conversations.
  • Thread view: chronological message bubbles (sender right, recipient left). Rich text input with emoji picker and FileUpload for media attachments. "Send" button. Scroll-to-bottom on new message. Marks as read on open.
  • New conversation: initiated from a talent/producer profile page via a "Message" button. Opens the thread directly.
  • Poll every 15 s for new messages (WebSocket subscription in a future enhancement).