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:
- Direct messages between coach and member (and staff-to-staff) inside an organization. Plain text + image / video attachments. Stored in
messageswithworkout_assignment_id = null. - Workout-anchored comment threads — same
messagestable withworkout_assignment_idset; 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:
- Exercise comments — pinned discussions on a
workout_movement(template-level, not assignment-level), stored inexercise_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
| Persona | Surface | Capabilities |
|---|---|---|
| Member | Mobile chat tab; web /dashboard/messages | DM staff; comment on own workouts; read receipts |
| Coach / admin / owner | Web inbox + per-program tabs + workout drawer | DM any member; comment on any workout; pin-comment on exercises |
| Owner | Same | Same plus moderation (soft delete) |
Capabilities
- Send DM —
POST /organizations/:orgId/messageswith{ recipientMembershipId, content?, attachmentIds? }. Allowed pairs: staff↔staff, staff↔member, member→staff. Member↔member blocked. - Conversations list —
GET /conversationsreturns paginated DM threads (one row per other-participant) with unread count. - Message history — cursor-paginated.
- Read receipts —
PUT /messages/:id/readorPUT /conversations/:membershipId/read(bulk). - Soft delete —
DELETE /messages/:idsetsdeleted_at; UI hides; thread shows “message deleted” placeholder. - Edits —
editedAtcolumn on messages (no API surface yet). - Attachments — image / video via presigned PUT; thumbnail r2 key stored.
- Workout-anchored comments —
POST /workout-assignments/:assignmentId/comments; per-program unread counts; per-program recent comment feed. - Push fan-out — every new message triggers
PushNotificationsService.notifyUserwith categorynewMessageornewComment(workout-anchored). - WebSocket gateway (
messages.gateway.ts) — connected clients receive realtimemessage:new,announcement:new, presenceuser: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.
Related
../announcements/— uses the same WebSocket gateway for broadcast.../push-notifications/— fan-out target.../workouts/and../workout-assignments/— host the FK target.../uploads-r2/— staged-PUT for attachments.
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)