Skip to content

Messaging

The messaging module provides a two-panel messaging interface at /messaging. It combines SMS messages, emails, and call records into a single chronological feed per contact, with real-time delivery via WebSocket events.

PathComponentPurpose
/messagingMessagingViewContact list + empty state
/messaging/:contact_idMessagingContactViewContact list + communication feed
pub struct Message {
pub id: Uuid,
pub organization_id: Uuid,
pub contact_id: Option<Uuid>,
pub phone_number_id: Option<Uuid>,
pub channel: MessageChannel,
pub direction: MessageDirection,
pub body: Option<String>,
pub from_address: String,
pub to_address: String,
pub status: MessageStatus,
pub source: Option<String>,
pub ai_origin: Option<AiOrigin>,
pub subject: Option<String>,
pub external_id: Option<String>,
pub created_at: String,
pub attachments: Vec<MessageAttachment>,
pub phone_line_label: Option<String>,
pub receiver_number_display: Option<String>,
}
pub struct MessageAttachment {
pub id: Uuid,
pub message_id: Option<Uuid>,
pub organization_id: Uuid,
pub storage_key: String,
pub content_type: String,
pub file_name: Option<String>,
pub file_size: Option<i32>,
pub created_at: String,
}
pub enum MessageDirection { Inbound, Outbound }
pub enum MessageStatus { Queued, Sent, Delivered, Failed, Received }
pub enum MessageChannel { Sms, Whatsapp, FacebookMessenger, InstagramDm, Email }
pub enum CommunicationFeedFilter { All, Calls, Messages, Emails }

Sms and Email channels are supported for sending. See the Email Channel page for email-specific details. Other channels return a 400 error.

POST /api/messages/send
ParameterTypeRequiredDescription
contact_idUuidYesTarget contact
phone_number_idUuidSMSOrg phone number to send from
channelMessageChannelYesSms or Email
bodyStringYesMessage text (markdown for email)
attachment_idsVec<Uuid>NoPre-uploaded attachment IDs for MMS
email_address_idUuidEmailOrg email address to send from
subjectStringEmailEmail subject line
contact_phone_idUuidNoSpecific recipient phone (defaults to preferred)
contact_email_idUuidNoSpecific recipient email (defaults to preferred)
ai_originStringNoAI feature that composed this message — see AI Origin Tracking

Returns the created Message with linked attachments. When attachment_ids are provided:

  1. Each attachment must belong to the org and have no existing message_id (pending state)
  2. Presigned R2 URLs are generated for each attachment
  3. Twilio sends the message as MMS with media URLs
  4. Attachments are linked to the new message record
GET /api/messages?contact_id={uuid}

Returns all messages for a contact within the org, sorted chronologically.

MessagingView and MessagingContactView use a full-bleed 4-column grid:

  • Left panel (1/4): MessagingContactList — searchable sidebar with debounced search (300ms), sorted by last contacted date. Each row shows the contact name, primary phone, and an aging indicator dot.
  • Right panel (3/4): Either an empty state prompt or the CommunicationFeed for the selected contact.

CommunicationFeed merges calls and messages into a single chronological timeline:

  • Groups items by date with divider headers
  • Filter bar with four tabs: All, Calls, Messages, Emails
  • Auto-scrolls to the latest entry on load and new messages
  • Supports an image lightbox for viewing attachments across the conversation

MessageEntry renders directional chat bubbles:

  • Outbound — right-aligned with chat-outbound-bg / chat-outbound-border CSS variables
  • Inbound — left-aligned with chat-inbound-bg / chat-inbound-border CSS variables
  • Status indicator on outbound messages: Sending…, Sent, Delivered, Failed
  • Email messages show a mail icon and render the subject line above the body
  • Clicking an inbound message opens the InlineSuggestionsPanel for AI-suggested replies

MessagingCallEntry renders compact, directional call cards (max 38% width):

  • Outbound cards align right with a primary-tinted border
  • Inbound cards align left with a standard border
  • Collapsible sections for transcription and AI analysis
  • Transcription and analysis text render as markdown with the prose-sm class for compact sizing

MessagingContactView selects the outbound phone number automatically before passing it to the compose bar:

  1. Scans the contact’s messages in reverse chronological order for the most recent inbound message
  2. Uses that message’s phone_number_id — so replies go out from the same number the contact texted
  3. Falls back to the org’s first phone number only when no inbound message history exists (e.g., starting a new conversation)

This ensures contacts always see replies from the number they originally messaged.

MessageCompose sits at the bottom of the feed:

  • “To:” recipient selector — displays the contact phone number (SMS) or email address (Email) the message will be sent to. When the contact has multiple phones or emails, a dropdown lets you choose; when only one exists, it displays as a read-only label. Defaults to the contact’s preferred entry.
  • Text area with Ctrl+Enter to send
  • Paperclip button opens a file picker for attachments (uploads via /api/messages/upload)
  • Pending attachments display as image thumbnails or file chips with remove buttons
  • An optional body_signal prop lets parent components pre-populate the compose field (used by the AI suggestions panel)

The recipient selector passes contact_phone_id (SMS) or contact_email_id (Email) to send_message_api, overriding the default auto-resolution of the preferred address. Recipients display with their label when available — e.g., +17865551234 (mobile) or user@example.com (personal).

PropTypeDescription
contact_phonesVec<ContactPhone>Contact’s phone numbers — enables “To:” selector for SMS
contact_emailsVec<ContactEmail>Contact’s email addresses — enables “To:” selector for Email
pub struct ContactPhone {
pub id: Uuid,
pub phone_number: String,
pub label: Option<String>, // e.g., "mobile", "work"
pub is_preferred: bool,
}
pub struct ContactEmail {
pub id: Uuid,
pub email: String,
pub label: Option<String>, // e.g., "personal", "work"
pub is_preferred: bool,
}

MessagingContactView extracts contact_phones and contact_emails from the contact resource and threads them through CommunicationFeed down to MessageCompose.

The use_realtime_messages hook subscribes to the shared RealtimeContext WebSocket and filters for two event types:

EventConstantBehavior
New messagemessaging.message.newAppends the message to the live feed (or updates attachments if the ID already exists)
Status changemessaging.message.statusUpdates the delivery status on an existing message

Events are scoped by contact_id — only messages matching the currently viewed contact appear.

  1. User types a message and optionally attaches files
  2. MessagingContactView resolves the phone_number_id from the contact’s last inbound message (see Phone Number Routing)
  3. MessageCompose calls send_message_api with the contact, phone, channel, body, attachment IDs, and the selected contact_phone_id or contact_email_id
  4. Server looks up the org’s Twilio credentials from the settings table
  5. Server uses the explicitly selected contact phone/email, or falls back to the preferred one
  6. For MMS: generates presigned R2 URLs for each attachment, calls send_mms
  7. For SMS: calls send_sms directly
  8. Creates a Message record with status Queued and links any attachments
  9. Publishes a messaging.message.new event to the org’s real-time channel
  10. Asynchronously updates the contact’s last_contacted_at timestamp
FilePurpose
src/mods/messaging/views/messaging_view.rs/messaging route — layout + empty state
src/mods/messaging/views/messaging_contact_view.rs/messaging/:contact_id route — feed + compose
src/mods/messaging/components/communication_feed_component.rsMerged call + message timeline
src/mods/messaging/components/message_entry_component.rsChat bubble rendering
src/mods/messaging/components/message_compose_component.rsCompose bar with file upload
src/mods/messaging/components/messaging_call_entry_component.rsCompact call cards
src/mods/messaging/components/messaging_contact_list_component.rsSidebar contact list
src/mods/messaging/hooks/use_realtime_messages.rsWebSocket message subscription
src/mods/messaging/api/send_message_api.rsSend SMS/MMS endpoint
src/mods/messaging/api/get_contact_messages_api.rsFetch messages endpoint
src/mods/messaging/services/create_message_service.rsDatabase insert
src/mods/messaging/services/attachment/Attachment CRUD + linking