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.
Routes
Section titled “Routes”| Path | Component | Purpose |
|---|---|---|
/messaging | MessagingView | Contact list + empty state |
/messaging/:contact_id | MessagingContactView | Contact list + communication feed |
Data Model
Section titled “Data Model”Message
Section titled “Message”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>,}MessageAttachment
Section titled “MessageAttachment”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.
API Endpoints
Section titled “API Endpoints”Send a Message
Section titled “Send a Message”POST /api/messages/send| Parameter | Type | Required | Description |
|---|---|---|---|
contact_id | Uuid | Yes | Target contact |
phone_number_id | Uuid | SMS | Org phone number to send from |
channel | MessageChannel | Yes | Sms or Email |
body | String | Yes | Message text (markdown for email) |
attachment_ids | Vec<Uuid> | No | Pre-uploaded attachment IDs for MMS |
email_address_id | Uuid | Org email address to send from | |
subject | String | Email subject line | |
contact_phone_id | Uuid | No | Specific recipient phone (defaults to preferred) |
contact_email_id | Uuid | No | Specific recipient email (defaults to preferred) |
ai_origin | String | No | AI feature that composed this message — see AI Origin Tracking |
Returns the created Message with linked attachments. When attachment_ids are provided:
- Each attachment must belong to the org and have no existing
message_id(pending state) - Presigned R2 URLs are generated for each attachment
- Twilio sends the message as MMS with media URLs
- Attachments are linked to the new message record
Get Contact Messages
Section titled “Get Contact Messages”GET /api/messages?contact_id={uuid}Returns all messages for a contact within the org, sorted chronologically.
UI Components
Section titled “UI Components”Layout
Section titled “Layout”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
CommunicationFeedfor the selected contact.
Communication Feed
Section titled “Communication Feed”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
Chat Bubbles
Section titled “Chat Bubbles”MessageEntry renders directional chat bubbles:
- Outbound — right-aligned with
chat-outbound-bg/chat-outbound-borderCSS variables - Inbound — left-aligned with
chat-inbound-bg/chat-inbound-borderCSS 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
InlineSuggestionsPanelfor AI-suggested replies
Call Cards
Section titled “Call Cards”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-smclass for compact sizing
Phone Number Routing
Section titled “Phone Number Routing”MessagingContactView selects the outbound phone number automatically before passing it to the compose bar:
- Scans the contact’s messages in reverse chronological order for the most recent inbound message
- Uses that message’s
phone_number_id— so replies go out from the same number the contact texted - 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.
Compose Bar
Section titled “Compose Bar”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_signalprop 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).
Recipient Selector Props
Section titled “Recipient Selector Props”| Prop | Type | Description |
|---|---|---|
contact_phones | Vec<ContactPhone> | Contact’s phone numbers — enables “To:” selector for SMS |
contact_emails | Vec<ContactEmail> | Contact’s email addresses — enables “To:” selector for Email |
ContactPhone / ContactEmail Types
Section titled “ContactPhone / ContactEmail Types”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.
Real-Time Updates
Section titled “Real-Time Updates”The use_realtime_messages hook subscribes to the shared RealtimeContext WebSocket and filters for two event types:
| Event | Constant | Behavior |
|---|---|---|
| New message | messaging.message.new | Appends the message to the live feed (or updates attachments if the ID already exists) |
| Status change | messaging.message.status | Updates the delivery status on an existing message |
Events are scoped by contact_id — only messages matching the currently viewed contact appear.
Outbound Message Flow
Section titled “Outbound Message Flow”- User types a message and optionally attaches files
MessagingContactViewresolves thephone_number_idfrom the contact’s last inbound message (see Phone Number Routing)MessageComposecallssend_message_apiwith the contact, phone, channel, body, attachment IDs, and the selectedcontact_phone_idorcontact_email_id- Server looks up the org’s Twilio credentials from the
settingstable - Server uses the explicitly selected contact phone/email, or falls back to the preferred one
- For MMS: generates presigned R2 URLs for each attachment, calls
send_mms - For SMS: calls
send_smsdirectly - Creates a
Messagerecord with statusQueuedand links any attachments - Publishes a
messaging.message.newevent to the org’s real-time channel - Asynchronously updates the contact’s
last_contacted_attimestamp
Key Files
Section titled “Key Files”| File | Purpose |
|---|---|
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.rs | Merged call + message timeline |
src/mods/messaging/components/message_entry_component.rs | Chat bubble rendering |
src/mods/messaging/components/message_compose_component.rs | Compose bar with file upload |
src/mods/messaging/components/messaging_call_entry_component.rs | Compact call cards |
src/mods/messaging/components/messaging_contact_list_component.rs | Sidebar contact list |
src/mods/messaging/hooks/use_realtime_messages.rs | WebSocket message subscription |
src/mods/messaging/api/send_message_api.rs | Send SMS/MMS endpoint |
src/mods/messaging/api/get_contact_messages_api.rs | Fetch messages endpoint |
src/mods/messaging/services/create_message_service.rs | Database insert |
src/mods/messaging/services/attachment/ | Attachment CRUD + linking |