Internal Chat
Purpose
Section titled “Purpose”Describe the built-in internal chat: who can message whom, how conversations are modeled, and how messages are delivered in real time.
Overview
Section titled “Overview”Chat is tenant-scoped text messaging between users of the same tenant. It serves two needs:
- Escalation — an agent can always direct-message a supervisor or admin (and vice versa), regardless of any tenant setting.
- Peer chat — agents who share at least one campaign can DM each other, and
supervisors/admins can create group threads. This is gated by a per-tenant
toggle (
tenants.peer_chat_enabled, off by default).
Conversations come in two kinds (chat_conversations.kind): direct (1:1) and
group. “Escalation” is not a separate kind — it is simply a direct thread
whose other party is privileged.
Configuration
Section titled “Configuration”The data model (migration 066):
chat_conversations—kind(direct/group),title(group only),pair_key(the two user IDs sorted + joined, unique per tenant for DMs),created_by.chat_participants— membership plus a singlelast_read_atread cursor per user.chat_messages— append-onlybodytext (no attachments, edits, or deletes), indexed by conversation + time.
All three tables carry tenant_id and enforce Postgres RLS.
Endpoints (all authenticated tenant users; non-members get 404 so existence
isn’t revealed):
| Method | Path | Purpose |
|---|---|---|
| GET·POST | /api/v1/chat/conversations |
List threads (with unread counts) / open a DM or create a group |
| GET·POST | /api/v1/chat/conversations/{id}/messages |
Page messages / send a message |
| POST | /api/v1/chat/conversations/{id}/read |
Advance the read cursor |
| GET·PUT | /api/v1/chat/settings |
Read / toggle peer_chat_enabled (PUT is supervisor/admin only) |
Examples
Section titled “Examples”Who can start what (enforced server-side):
- A privileged user (supervisor/admin/platform_admin) can DM anyone — that’s escalation, always on.
- Two agents can DM only when
peer_chat_enabledis on and they share a campaign. - Group creation is supervisor/admin only, and every member is individually checked so a supervisor can’t bridge two agents who couldn’t otherwise talk.
Realtime delivery uses a dedicated WebSocket, GET /ws/chat (JWT via header
or ?token=). Messages fan out over the NATS subject
t.<tenant>.chat.<conversation_id>.message; the server filters each socket to
conversations the user belongs to (using the participant_user_ids carried on
the payload, no extra DB hit). Publishing is best-effort — a message is durably
stored even if the fan-out fails, and loads on the next fetch.
- Read state is a single
last_read_atcursor per participant; unread = messages newer than it not sent by you. No per-message read receipts, typing or presence. - Sending advances your own read cursor and floats the thread to the top of everyone’s list.
- Messages are append-only with no retention/TTL — rows are removed only by
ON DELETE CASCADEwhen the tenant, conversation or user is deleted. - Message pages default to 50, capped at 200.