SoConnective

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):

CollectionKey fields
conversationssubject, channel, status, contact, company, identity, assignee, externalId, lastMessageAt, lastMessagePreview, lastDirection, unread
messagesconversation (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.ts
  • apps/cms/src/hooks/conversations.tsonMessageAfterChange (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.tsx
  • apps/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 + externalId on 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 activities so 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 + messages tables 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.
Previous
Phase 1 — CRM Core (+ Phase 1.5 polish)