Product
Phase 2 — Omnichannel Inbox
Status: Delivered (2026-06-12). Built natively over the Identities graph. No Chatwoot (ADR 0006).
Scope delivered
A native, tenant-scoped conversations system and a working inbox UI:
- Conversation list + thread + composer at
/dashboard/inbox(two-pane). - Reply (outbound message), status change (Open / Pending / Snoozed / Closed), new conversation, and simulate inbound (records an inbound message until real channel connectors land).
- Every message rolls up onto its conversation (last message time, preview, direction, unread) and writes an entry into the activity timeline (Inbound Message / Outbound Message), so the 360 stays the single source of truth.
- The contact 360 inbox placeholder now shows that contact's real conversations, linking into the inbox.
- Sidebar gains an Inbox entry under CRM.
Data-model changes
Two new tenant-scoped collections (multiTenant plugin + RLS):
| Collection | Key fields |
|---|---|
conversations | subject, channel, status, contact, company, identity, assignee, externalId, lastMessageAt, lastMessagePreview, lastDirection, unread |
messages | conversation (req), direction (Inbound/Outbound), body, preview (auto), channel, author, identity, externalId, sentAt, status |
identities (already present) remains the cross-channel graph that links a handle (phone / @handle / email) to a contact; conversations point at both the contact and the originating identity.
Schema shipped via a committed Payload migration (20260612_210526_phase2_conversations), generated + applied in the prod CMS container against the live DB — push:true is dev-only (G-24 / ADR 0007).
Key files
apps/cms/src/collections/Conversations.ts,Messages.tsapps/cms/src/hooks/conversations.ts—onMessageAfterChange(rollup + timeline; tenant-normalized like the automation engine, G-21)apps/crm/lib/inbox-actions.ts— server actions (get/send/simulate/status/ start + per-contact fetch)apps/crm/app/dashboard/(auth)/inbox/page.tsx+inbox-client.tsxapps/crm/components/layout/sidebar/nav-main.tsx(Inbox entry)apps/crm/app/dashboard/(auth)/contacts/[id]/page.tsx(360 conversations)
Decisions
- Built natively (no Chatwoot): the identity graph is the product's moat, so conversations live inside SoConnective (ADR 0006).
- Channel-agnostic data model: a generic
channel+externalIdon both conversation and message lets any connector (WhatsApp, email, IG…) post into the same inbox later without schema changes. - Timeline as source of truth: messages feed
activitiesso a contact's whole history (deals, tasks, appointments, messages, automation notes) reads from one place.
GATE results (verified live on prod, commit 488b651)
- CMS + CRM type-check: 0 errors.
- Migration applied;
conversations+messagestables exist in the live DB. - End-to-end via API as a tenant-scoped user: conversation + inbound + outbound created (201); the hook rolled up onto the conversation (
lastDirection=Outbound,unread=false, correct preview) and wrote Inbound/Outbound activities to the timeline. - Inbox UI renders (HTTP 200, 0 console/page errors): list, thread bubbles (inbound left / outbound right), status select, composer.
- Tenant isolation confirmed (a user without the tenant is denied read/write).
- All test data + the throwaway test user were removed afterwards.
Deferred (explicit)
- Real channel connectors — WhatsApp Cloud API, IMAP/SMTP email, Instagram/ Messenger webhooks. The data model + a generic ingest path are ready; the connectors and a signed inbound webhook are the next increment.
- Real-time — currently router-refresh after send; websockets/SSE later.
- AI auto-reply / drafting — Phase 4.
- Assignment & routing rules, canned replies, attachments, read receipts.