Overview
- Conversation rows, not message rows. Each row rolls up a whole thread: an outbound send and its replies, or an inbound submission and your responses.
- Recipient-first display. Rows are keyed to who the conversation is with - one contact, a pair, or a group.
- Draft, thread, and channel signals. A Draft chip, a thread counter, a WhatsApp badge, and a relative timestamp sit on each row.
- Click to open. Selecting a row opens the thread view, or the compose pane if the row is still a draft.
- Real-time. New inbound replies appear without a manual refresh.
- Scoped by role. Admins and managers see the whole organization; instructors see only conversations for members at the clubs they teach.
How the list is built
The list queries/v1/messages filtered to top-level rows (those with no replyToMessageId). Each row is therefore a conversation head: outbound sends with their replies, inbound submissions with your responses, and drafts not yet sent.
Rows are sorted by createdAt descending and paginated 25 at a time. Load more at the bottom extends the page in 25-row steps.
What a row shows
Each row is built around the counterparty of the conversation:- A single contact shows that contact’s avatar and full name.
- A pair of contacts shows two stacked avatars and “Firstname, Firstname”.
- A group of 3+ shows the first contact’s avatar with a
+Nbadge and “Firstname + N others”. - An inbound message with no stored recipients falls back to the sender’s avatar and name - this is how a member-initiated conversation renders.
- A WhatsApp badge next to a single contact whose WhatsApp number is verified.
- A Draft status chip when the message has not been sent yet.
- The relative time of the latest message (
sentAtif sent,createdAtif still a draft). - A thread counter badge (the small number bubble) showing how many messages are in the conversation, including the original.
Channels and direction
A message carries a channel and a direction, both stored on the record.- Channel (
type) isemail,sms, orwhatsapp. The same channel-neutral content is projected to whichever channel a message was sent on. - Direction marks transport provenance:
outbound= sent from the platform,inbound= received from outside (for example a member’s email into the club’s inbox). Direction reflects provenance, not authorship - a member composing in the portal or app produces an outbound message addressed to staff, because it originated on the platform.
/v1/messages endpoint accepts filters for contactId, status, channel type, inboxId, and direction, which is how the same list powers scoped views (see below).
When a message is sent on behalf of a specific person - today, a class instructor via the “send from the instructor” option - the inbox shows that person’s name and avatar as the sender, matching the email the member received. The record’s underlying
senderId still points at the staff user accountable for the send.Selecting a row
Clicking a row updates the URL to/messages/<id> and renders the right pane:
- If the message is a draft, the compose pane opens so you can finish writing.
- Otherwise, the thread view renders: the original message at the top, replies underneath in order, and an inline reply box at the bottom.
/messages/<id>/reply and the compose pane takes over the right side with the original sender pre-selected as the recipient and the subject pre-filled with Re: <subject>.
On desktop, opening the inbox with nothing selected auto-opens the first conversation so the detail pane is not blank. On mobile the list stays put until you pick a row.
Real-time updates
The inbox subscribes to the notification WebSocket. Every time amessage-category event for the current organization arrives, the list and the open thread refetch, so new inbound replies appear without a manual refresh.
Status and engagement signals
The message-levelstatus is simple: draft or sent. The inbox uses it to decide whether selecting a row opens compose or the thread view.
Per-recipient delivery detail is tracked on the record but not surfaced directly in the list. Each recipient has a deliveryStatus that can be queued, sent, processed, delivered, deferred, bounced, dropped, failed, spam, unsubscribed, or skipped, plus an engagementStatus of open, click, unsubscribe, or spamreport, with timestamps for each transition. Webhook handlers from SendGrid (email) and Twilio (SMS) update these fields as events arrive.
Audience scope: admin vs instructor view
- Admins and managers see the organization-wide inbox: every message attached to the org. The right-pane Compose button is enabled, and the audience filter and template picker are available when composing.
- Instructors see a club-scoped slice: conversations involving members at the clubs they teach at. Their compose form hides the audience filter and template picker; replying to existing inbound messages still works.
Permissions
- Reading the inbox requires
messages.read.own(broadermessages.read.club/messages.read.organizationholders see more). - Composing or replying requires a
messages.createpermission. - The audience filter and organization-wide templates require manager or admin scope.
Worked examples
Spotting a member reply (climbing gym)
A boulderer replied to your route-setting update. Because the inbox refetches on the WebSocket event, the conversation jumps with an incremented thread counter and a fresh timestamp - no refresh needed. Click it to open the thread and reply inline.Finishing a draft blast (martial arts)
You started a message to all jiu-jitsu members about a belt-grading date but did not send it. It sits in the list with a Draft chip. Clicking the row opens the compose pane instead of a thread, so you can finish and send it.Reading a group thread (racket sports)
You messaged four padel members about a doubles ladder. The row shows the first member’s avatar with a+3 badge and “Firstname + 3 others”. Opening it shows the single outbound message and any replies threaded beneath.
Tips & best practices
- Watch the thread counter. A rising number bubble means new replies have landed in that conversation.
- Let the auto-select help on desktop. The first conversation opens on load, so you can triage without an extra click.
- Use Reply, not a fresh compose, to keep threads intact. Replying preserves the conversation and pre-fills the recipient and
Re:subject. - Load more, then scroll. The list starts at 25 rows; extend it before hunting for an older conversation.
- Verify WhatsApp before relying on it. The WhatsApp badge tells you a contact’s number is verified and reachable on that channel.
Troubleshooting
Related
- Compose - The send and reply flow the right pane drives.
- Templates - Templates the compose pane reads from.
- Messaging overview - How the inbox fits with automations and AI replies.
- Automations - Automated, trigger-based sends that also land in the inbox.