Chat Architecture
Vidivo’s chat system provides real-time messaging between hosts and their clients. It is built with a WebSocket hub for instant delivery and REST endpoints for persistence and history retrieval.
Architecture Overview
Section titled “Architecture Overview” Client (Browser/App) | | REST: GET/POST messages, conversations | WebSocket: real-time delivery v API Gateway (Echo v5) | +----> Chat Handler (REST endpoints) | | | v | Chat Service (business logic + access control) | | | v | Chat Repository (PostgreSQL) | +----> Chat Hub (WebSocket connections) | v ChatClient (per-user connection) | v WritePump / ReadPump (goroutines)Data Model
Section titled “Data Model”Conversations
Section titled “Conversations”Each conversation is a 1-to-1 channel between two users. Conversations are lazily created when the first message is sent or when explicitly requested via the API.
| Field | Type | Description |
|---|---|---|
id | UUID | Unique conversation identifier |
created_at | timestamp | When the conversation was created |
updated_at | timestamp | Last message timestamp |
Participants
Section titled “Participants”A join table linking users to conversations with read tracking:
| Field | Type | Description |
|---|---|---|
conversation_id | UUID | Conversation reference |
user_id | UUID | Participant reference |
last_read_at | timestamp | When the participant last read the conversation |
Messages
Section titled “Messages”| Field | Type | Description |
|---|---|---|
id | UUID | Message identifier |
conversation_id | UUID | Parent conversation |
sender_id | UUID | Who sent the message |
body | text | Message content |
type | string | text or file |
file_url | string | URL for file attachments |
file_name | string | Original file name |
file_size | integer | File size in bytes |
file_type | string | MIME type |
created_at | timestamp | When the message was sent |
Access Control
Section titled “Access Control”Who Can Send Messages
Section titled “Who Can Send Messages”| Sender Role | Can Send? | Condition |
|---|---|---|
host | Always | No restrictions |
admin | Always | No restrictions |
user / verified_user | During active call only | Must have an active call_sessions record with the recipient |
guest | During active call only | Must have an active call session |
Enforcement
Section titled “Enforcement”The access control is enforced in the service layer (canSendMessage function):
- Check the sender’s role
- If the sender is a
hostoradmin, allow the message - Otherwise, query the
call_sessionstable for an active session between the sender and the recipient - If no active session exists, return
403 Forbidden
This design ensures that:
- Hosts can communicate with their clients at any time (scheduling, follow-up)
- Clients cannot use the platform for general-purpose messaging
- The chat feature serves its purpose as a professional communication tool
WebSocket Protocol
Section titled “WebSocket Protocol”Connection
Section titled “Connection”Clients connect to the WebSocket endpoint with their JWT access token:
wss://api.vidivo.app/v1/chat/ws?token=<access_token>The handler validates the JWT, extracts the user ID, and registers the connection with the ChatHub.
Hub Architecture
Section titled “Hub Architecture”The ChatHub maintains a map of user_id -> ChatClient:
ChatHub | +-- clients map[uuid.UUID]*ChatClient | +-- register chan *ChatClient (new connections) +-- unregister chan *ChatClient (disconnections) +-- broadcast chan []byte (messages to deliver)Each ChatClient runs two goroutines:
- ReadPump: Reads incoming WebSocket messages and routes them to the service layer
- WritePump: Writes outgoing messages to the WebSocket connection
Message Flow
Section titled “Message Flow” Sender Client Server Recipient Client | | | |-- WS message ---------->| | | | | | Validate access | | Persist to DB | | Find recipient hub | | | | | |-- WS new_message --------->| | | |If the recipient is not connected (offline), the message is only persisted. They will see it when they next open the conversation via the REST API.
Message Types
Section titled “Message Types”Client to Server:
| Type | Fields | Description |
|---|---|---|
message | conversation_id, body | Send a text message |
typing | conversation_id | Send typing indicator |
read | conversation_id | Mark conversation as read |
Server to Client:
| Type | Fields | Description |
|---|---|---|
new_message | message object | New message received |
typing | conversation_id, user_id | Other user is typing |
read_receipt | conversation_id, user_id | Other user read the conversation |
File Sending
Section titled “File Sending”Messages with type: "file" include file metadata:
{ "body": "Here is the document", "type": "file", "file_url": "https://cdn.vidivo.app/files/session-notes.pdf", "file_name": "session-notes.pdf", "file_size": 245760, "file_type": "application/pdf"}Files are uploaded separately (to R2 storage) and then referenced in the message. The chat system does not handle file uploads directly --- it stores the URL reference.
Broadcast System
Section titled “Broadcast System”Hosts can broadcast a message to all of their followers:
Host sends broadcast | v Service gets all follower IDs (from follow repo) | v For each follower: 1. Get or create conversation with follower 2. Create message in conversation 3. If follower is connected to hub, deliver via WebSocketThis fan-out approach ensures each follower has their own conversation thread with the host, keeping the chat personal rather than group-based.
Concurrency Design
Section titled “Concurrency Design”The ChatHub uses a single event loop pattern (similar to Redis) to avoid locks:
func (h *ChatHub) Run(ctx context.Context) { for { select { case client := <-h.register: h.clients[client.UserID] = client case client := <-h.unregister: delete(h.clients, client.UserID) case <-ctx.Done(): return } }}This ensures thread-safe access to the clients map without mutex contention.