CRM messages are many-to-many, not single-attribution
Designing the data model that maps ingested messages from multiple chat platforms onto people in a personal CRM, particularly group chats and outbound messages.
The intuitive schema for a personal CRM that ingests messages from email, WhatsApp, iMessage etc. is messages.person_id pointing at the contact this message belongs to. That model breaks badly for two cases: (1) outbound messages — the user sends to Alice in 1:1, the user wants the conversation visible on Alices page AND on their own page as a what-I-sent log, but a single person_id forces one or the other; (2) group chats — the user sends to Bob and Carol in a group, each recipient should see the message in their conversation history with the user, but a single person_id can only point at one of them. Forcing single-attribution corrupts the CRM in either direction: pick the sender and group recipients lose visibility; pick the OTHER party and ambiguous-group messages get triaged forever. The right shape is a participants index table — message_id, person_id, role — that gives every visible person a row per message. Per-person timeline queries JOIN on participants. The canonical message body still lives in one JSONL per primary attribution, but visibility is many-to-many. This mirrors how email maps to a folder per participant rather than one folder per message and survives every cross-platform case. Single-attribution is OK as a UI hint about WHO the message is most-about, but it should never be the only index a per-person timeline query uses.
When designing a personal-CRM ingest pipeline, model message-to-person visibility as many-to-many via a participants table from day one. Keep a single primary attribution column for display ordering or triage, but query per-person timelines via JOIN on the participants index — never via WHERE person_id = ?.