Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesMessages CommentsMessages + Comments

Messages + Comments

Linear epics: FIT-155 (workout-anchored comments unified onto messages), prior coach-workout-discussions PRD (archived at docs/_archive/product/coach-workout-discussions-prd.md — superseded). Status: Shipped (DMs + workout-anchored threads). WebSocket gateway active. Exercise-pinned comments (exercise_comments) shipped separately. Last reviewed: 2026-05-28

What

Two adjacent features sharing tables:

  1. Direct messages between coach and member (and staff-to-staff) inside an organization. Plain text + image / video attachments. Stored in messages with workout_assignment_id = null.
  2. Workout-anchored comment threads — same messages table with workout_assignment_id set; surfaced as a per-workout discussion drawer/panel rather than the inbox. Per FIT-155, this replaced a separate workout_comments table.

A third, smaller surface lives on its own table:

  1. Exercise comments — pinned discussions on a workout_movement (template-level, not assignment-level), stored in exercise_comments. Used in the workout builder + member profile per the original PRD §7.6 / §7.8.

Why

  • Coach ↔ member chat is the primary engagement channel for online-coaching orgs.
  • Workout-anchored threads keep “what’s wrong with this set” discussions adjacent to the workout, not buried in the inbox.
  • The unified table simplifies push fan-out (one notifier path) and read-receipt handling.

Personas

PersonaSurfaceCapabilities
MemberMobile chat tab; web /dashboard/messagesDM staff; comment on own workouts; read receipts
Coach / admin / ownerWeb inbox + per-program tabs + workout drawerDM any member; comment on any workout; pin-comment on exercises
OwnerSameSame plus moderation (soft delete)

Capabilities

  • Send DMPOST /organizations/:orgId/messages with { recipientMembershipId, content?, attachmentIds? }. Allowed pairs: staff↔staff, staff↔member, member→staff. Member↔member blocked.
  • Conversations listGET /conversations returns paginated DM threads (one row per other-participant) with unread count.
  • Message history — cursor-paginated.
  • Read receiptsPUT /messages/:id/read or PUT /conversations/:membershipId/read (bulk).
  • Soft deleteDELETE /messages/:id sets deleted_at; UI hides; thread shows “message deleted” placeholder.
  • EditseditedAt column on messages (no API surface yet).
  • Attachments — image / video via presigned PUT; thumbnail r2 key stored.
  • Workout-anchored commentsPOST /workout-assignments/:assignmentId/comments; per-program unread counts; per-program recent comment feed.
  • Push fan-out — every new message triggers PushNotificationsService.notifyUser with category newMessage or newComment (workout-anchored).
  • WebSocket gateway (messages.gateway.ts) — connected clients receive realtime message:new, announcement:new, presence user:online/user:offline, typing indicators. JWT-auth via Clerk.

Capabilities (gaps)

  • No file attachments yet for workout comments (text-only).
  • No message edit endpoint despite the column.
  • No moderation log / audit trail.

Source code

  • API: apps/api/src/messages/
  • DB: libs/db/src/lib/schema/messaging.ts (DMs + workout comments), comments.ts (exercise comments)
  • Web: apps/web/src/components/messages/ (DMs + workout panels)