[{"data":1,"prerenderedAt":1907},["ShallowReactive",2],{"content-vision\u002Fmulti-channel":3,"surround-\u002Fvision\u002Fmulti-channel":1898},{"id":4,"title":5,"body":6,"description":1891,"extension":1892,"meta":1893,"navigation":106,"path":1894,"seo":1895,"stem":1896,"__hash__":1897},"content\u002F5.vision\u002F4.multi-channel.md","Multi-Channel & CRM",{"type":7,"value":8,"toc":1876},"minimark",[9,14,24,29,32,538,541,554,559,665,672,676,683,1021,1033,1037,1040,1152,1155,1159,1169,1279,1290,1306,1309,1313,1316,1324,1330,1467,1471,1478,1482,1485,1610,1616,1637,1641,1644,1807,1810,1848,1852,1855,1869,1872],[10,11,13],"h1",{"id":12},"multi-channel-crm-architecture","Multi-Channel & CRM Architecture",[15,16,17,18,23],"p",{},"Owlat starts with email. But the ",[19,20,22],"a",{"href":21},"\u002Fvision#the-communication-hub","Communication Hub"," is channel-agnostic — every message becomes a structured event in the same pipeline, regardless of where it originates. This page covers the technical architecture for multi-channel support and the CRM hub that unifies contacts across all channels.",[25,26,28],"h2",{"id":27},"channel-adapter-interface","Channel adapter interface",[15,30,31],{},"Channel adapters are pluggable TypeScript classes that normalize different communication channels into a unified message format. They run as Convex actions — no separate services, no additional infrastructure.",[33,34,39],"pre",{"className":35,"code":36,"language":37,"meta":38,"style":38},"language-typescript shiki shiki-themes github-light github-dark-dimmed","interface ChannelAdapter {\n  \u002F** Unique channel identifier *\u002F\n  id: 'email' | 'sms' | 'whatsapp' | 'webhook' | 'chat'\n\n  \u002F** Send a message through this channel *\u002F\n  send(message: OutboundMessage): Promise\u003CSendResult>\n\n  \u002F** Parse an inbound webhook payload into a unified message *\u002F\n  parseInbound(raw: unknown): ParsedMessage\n\n  \u002F** Check delivery status of a sent message *\u002F\n  getDeliveryStatus(externalId: string): Promise\u003CDeliveryStatus>\n\n  \u002F** Validate an inbound webhook signature *\u002F\n  validateSignature(request: Request): Promise\u003Cboolean>\n\n  \u002F** Report current connection health *\u002F\n  healthCheck(): Promise\u003CChannelHealth>\n}\n\ninterface ChannelHealth {\n  status: 'healthy' | 'degraded' | 'down'\n  lastSuccessfulSend?: number    \u002F\u002F Timestamp\n  lastError?: string\n  rateLimitRemaining?: number    \u002F\u002F Provider rate limit headroom\n  latencyMs?: number             \u002F\u002F Average send latency\n}\n\ninterface OutboundMessage {\n  organizationId: string\n  contactId: string\n  channel: string\n  content: {\n    text?: string\n    html?: string\n    subject?: string        \u002F\u002F email only\n    mediaUrl?: string       \u002F\u002F SMS\u002FWhatsApp\n  }\n  threadId?: string\n  metadata?: Record\u003Cstring, string>\n}\n","typescript","",[40,41,42,59,66,101,108,114,149,154,160,184,189,195,224,229,235,264,269,275,295,301,306,316,337,352,363,376,389,394,399,408,418,428,438,448,458,468,481,494,500,510,533],"code",{"__ignoreMap":38},[43,44,47,51,55],"span",{"class":45,"line":46},"line",1,[43,48,50],{"class":49},"s7YZ4","interface",[43,52,54],{"class":53},"sOLd2"," ChannelAdapter",[43,56,58],{"class":57},"sYgZi"," {\n",[43,60,62],{"class":45,"line":61},2,[43,63,65],{"class":64},"sDN9O","  \u002F** Unique channel identifier *\u002F\n",[43,67,69,73,76,80,83,86,88,91,93,96,98],{"class":45,"line":68},3,[43,70,72],{"class":71},"stnAF","  id",[43,74,75],{"class":49},":",[43,77,79],{"class":78},"s-HuK"," 'email'",[43,81,82],{"class":49}," |",[43,84,85],{"class":78}," 'sms'",[43,87,82],{"class":49},[43,89,90],{"class":78}," 'whatsapp'",[43,92,82],{"class":49},[43,94,95],{"class":78}," 'webhook'",[43,97,82],{"class":49},[43,99,100],{"class":78}," 'chat'\n",[43,102,104],{"class":45,"line":103},4,[43,105,107],{"emptyLinePlaceholder":106},true,"\n",[43,109,111],{"class":45,"line":110},5,[43,112,113],{"class":64},"  \u002F** Send a message through this channel *\u002F\n",[43,115,117,121,124,127,129,132,135,137,140,143,146],{"class":45,"line":116},6,[43,118,120],{"class":119},"sPO5f","  send",[43,122,123],{"class":57},"(",[43,125,126],{"class":71},"message",[43,128,75],{"class":49},[43,130,131],{"class":53}," OutboundMessage",[43,133,134],{"class":57},")",[43,136,75],{"class":49},[43,138,139],{"class":53}," Promise",[43,141,142],{"class":57},"\u003C",[43,144,145],{"class":53},"SendResult",[43,147,148],{"class":57},">\n",[43,150,152],{"class":45,"line":151},7,[43,153,107],{"emptyLinePlaceholder":106},[43,155,157],{"class":45,"line":156},8,[43,158,159],{"class":64},"  \u002F** Parse an inbound webhook payload into a unified message *\u002F\n",[43,161,163,166,168,171,173,177,179,181],{"class":45,"line":162},9,[43,164,165],{"class":119},"  parseInbound",[43,167,123],{"class":57},[43,169,170],{"class":71},"raw",[43,172,75],{"class":49},[43,174,176],{"class":175},"sviXB"," unknown",[43,178,134],{"class":57},[43,180,75],{"class":49},[43,182,183],{"class":53}," ParsedMessage\n",[43,185,187],{"class":45,"line":186},10,[43,188,107],{"emptyLinePlaceholder":106},[43,190,192],{"class":45,"line":191},11,[43,193,194],{"class":64},"  \u002F** Check delivery status of a sent message *\u002F\n",[43,196,198,201,203,206,208,211,213,215,217,219,222],{"class":45,"line":197},12,[43,199,200],{"class":119},"  getDeliveryStatus",[43,202,123],{"class":57},[43,204,205],{"class":71},"externalId",[43,207,75],{"class":49},[43,209,210],{"class":175}," string",[43,212,134],{"class":57},[43,214,75],{"class":49},[43,216,139],{"class":53},[43,218,142],{"class":57},[43,220,221],{"class":53},"DeliveryStatus",[43,223,148],{"class":57},[43,225,227],{"class":45,"line":226},13,[43,228,107],{"emptyLinePlaceholder":106},[43,230,232],{"class":45,"line":231},14,[43,233,234],{"class":64},"  \u002F** Validate an inbound webhook signature *\u002F\n",[43,236,238,241,243,246,248,251,253,255,257,259,262],{"class":45,"line":237},15,[43,239,240],{"class":119},"  validateSignature",[43,242,123],{"class":57},[43,244,245],{"class":71},"request",[43,247,75],{"class":49},[43,249,250],{"class":53}," Request",[43,252,134],{"class":57},[43,254,75],{"class":49},[43,256,139],{"class":53},[43,258,142],{"class":57},[43,260,261],{"class":175},"boolean",[43,263,148],{"class":57},[43,265,267],{"class":45,"line":266},16,[43,268,107],{"emptyLinePlaceholder":106},[43,270,272],{"class":45,"line":271},17,[43,273,274],{"class":64},"  \u002F** Report current connection health *\u002F\n",[43,276,278,281,284,286,288,290,293],{"class":45,"line":277},18,[43,279,280],{"class":119},"  healthCheck",[43,282,283],{"class":57},"()",[43,285,75],{"class":49},[43,287,139],{"class":53},[43,289,142],{"class":57},[43,291,292],{"class":53},"ChannelHealth",[43,294,148],{"class":57},[43,296,298],{"class":45,"line":297},19,[43,299,300],{"class":57},"}\n",[43,302,304],{"class":45,"line":303},20,[43,305,107],{"emptyLinePlaceholder":106},[43,307,309,311,314],{"class":45,"line":308},21,[43,310,50],{"class":49},[43,312,313],{"class":53}," ChannelHealth",[43,315,58],{"class":57},[43,317,319,322,324,327,329,332,334],{"class":45,"line":318},22,[43,320,321],{"class":71},"  status",[43,323,75],{"class":49},[43,325,326],{"class":78}," 'healthy'",[43,328,82],{"class":49},[43,330,331],{"class":78}," 'degraded'",[43,333,82],{"class":49},[43,335,336],{"class":78}," 'down'\n",[43,338,340,343,346,349],{"class":45,"line":339},23,[43,341,342],{"class":71},"  lastSuccessfulSend",[43,344,345],{"class":49},"?:",[43,347,348],{"class":175}," number",[43,350,351],{"class":64},"    \u002F\u002F Timestamp\n",[43,353,355,358,360],{"class":45,"line":354},24,[43,356,357],{"class":71},"  lastError",[43,359,345],{"class":49},[43,361,362],{"class":175}," string\n",[43,364,366,369,371,373],{"class":45,"line":365},25,[43,367,368],{"class":71},"  rateLimitRemaining",[43,370,345],{"class":49},[43,372,348],{"class":175},[43,374,375],{"class":64},"    \u002F\u002F Provider rate limit headroom\n",[43,377,379,382,384,386],{"class":45,"line":378},26,[43,380,381],{"class":71},"  latencyMs",[43,383,345],{"class":49},[43,385,348],{"class":175},[43,387,388],{"class":64},"             \u002F\u002F Average send latency\n",[43,390,392],{"class":45,"line":391},27,[43,393,300],{"class":57},[43,395,397],{"class":45,"line":396},28,[43,398,107],{"emptyLinePlaceholder":106},[43,400,402,404,406],{"class":45,"line":401},29,[43,403,50],{"class":49},[43,405,131],{"class":53},[43,407,58],{"class":57},[43,409,411,414,416],{"class":45,"line":410},30,[43,412,413],{"class":71},"  organizationId",[43,415,75],{"class":49},[43,417,362],{"class":175},[43,419,421,424,426],{"class":45,"line":420},31,[43,422,423],{"class":71},"  contactId",[43,425,75],{"class":49},[43,427,362],{"class":175},[43,429,431,434,436],{"class":45,"line":430},32,[43,432,433],{"class":71},"  channel",[43,435,75],{"class":49},[43,437,362],{"class":175},[43,439,441,444,446],{"class":45,"line":440},33,[43,442,443],{"class":71},"  content",[43,445,75],{"class":49},[43,447,58],{"class":57},[43,449,451,454,456],{"class":45,"line":450},34,[43,452,453],{"class":71},"    text",[43,455,345],{"class":49},[43,457,362],{"class":175},[43,459,461,464,466],{"class":45,"line":460},35,[43,462,463],{"class":71},"    html",[43,465,345],{"class":49},[43,467,362],{"class":175},[43,469,471,474,476,478],{"class":45,"line":470},36,[43,472,473],{"class":71},"    subject",[43,475,345],{"class":49},[43,477,210],{"class":175},[43,479,480],{"class":64},"        \u002F\u002F email only\n",[43,482,484,487,489,491],{"class":45,"line":483},37,[43,485,486],{"class":71},"    mediaUrl",[43,488,345],{"class":49},[43,490,210],{"class":175},[43,492,493],{"class":64},"       \u002F\u002F SMS\u002FWhatsApp\n",[43,495,497],{"class":45,"line":496},38,[43,498,499],{"class":57},"  }\n",[43,501,503,506,508],{"class":45,"line":502},39,[43,504,505],{"class":71},"  threadId",[43,507,345],{"class":49},[43,509,362],{"class":175},[43,511,513,516,518,521,523,526,529,531],{"class":45,"line":512},40,[43,514,515],{"class":71},"  metadata",[43,517,345],{"class":49},[43,519,520],{"class":53}," Record",[43,522,142],{"class":57},[43,524,525],{"class":175},"string",[43,527,528],{"class":57},", ",[43,530,525],{"class":175},[43,532,148],{"class":57},[43,534,536],{"class":45,"line":535},41,[43,537,300],{"class":57},[15,539,540],{},"Every adapter owns its full lifecycle — not just message send\u002Freceive, but connection health, signature validation, and rate limit awareness. This prevents duplicated validation logic across webhook handlers and gives the monitoring system a unified health surface.",[15,542,543,544,548,549,553],{},"Adding a new channel means implementing this interface and registering an inbound webhook endpoint in the Convex HTTP router — the same pattern used for the existing ",[19,545,547],{"href":546},"\u002Fdeveloper\u002Fconvex","Resend"," and ",[19,550,552],{"href":551},"\u002Fdeveloper\u002Fmta-system","MTA"," webhooks.",[555,556,558],"h3",{"id":557},"built-in-adapters","Built-in adapters",[560,561,562,581],"table",{},[563,564,565],"thead",{},[566,567,568,572,575,578],"tr",{},[569,570,571],"th",{},"Adapter",[569,573,574],{},"Outbound",[569,576,577],{},"Inbound",[569,579,580],{},"Provider",[582,583,584,602,618,633,649],"tbody",{},[566,585,586,593,596,599],{},[587,588,589],"td",{},[590,591,592],"strong",{},"Email",[587,594,595],{},"MTA \u002F SES \u002F Resend (existing)",[587,597,598],{},"MTA inbound SMTP",[587,600,601],{},"Self-hosted or cloud",[566,603,604,609,612,615],{},[587,605,606],{},[590,607,608],{},"SMS",[587,610,611],{},"Twilio \u002F Vonage API",[587,613,614],{},"Webhook from provider",[587,616,617],{},"Cloud API",[566,619,620,625,628,631],{},[587,621,622],{},[590,623,624],{},"WhatsApp",[587,626,627],{},"WhatsApp Business API",[587,629,630],{},"Webhook from Meta",[587,632,617],{},[566,634,635,640,643,646],{},[587,636,637],{},[590,638,639],{},"Webhook",[587,641,642],{},"HTTP POST to external URL",[587,644,645],{},"HTTP POST from external system",[587,647,648],{},"Any",[566,650,651,656,659,662],{},[587,652,653],{},[590,654,655],{},"Chat",[587,657,658],{},"Convex real-time (native)",[587,660,661],{},"Convex mutation (native)",[587,663,664],{},"Built-in",[15,666,667,668,671],{},"The email adapter wraps the existing email provider system (",[40,669,670],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FemailProviders\u002F","). SMS and WhatsApp adapters are API clients that run within Convex actions. The chat adapter is native — messages go directly into Convex tables with real-time subscription updates.",[25,673,675],{"id":674},"unified-message-model","Unified message model",[15,677,678,679,682],{},"All messages across all channels flow into a single ",[40,680,681],{},"unifiedMessages"," table:",[33,684,686],{"className":35,"code":685,"language":37,"meta":38,"style":38},"unifiedMessages: defineTable({\n  organizationId: v.string(),\n  threadId: v.id('conversationThreads'),\n  channel: v.string(),\n  direction: v.union(v.literal('inbound'), v.literal('outbound')),\n  contactId: v.optional(v.id('contacts')),\n  memberId: v.optional(v.string()),     \u002F\u002F internal sender (BetterAuth user ID)\n  content: v.string(),                   \u002F\u002F JSON: { text, html, subject, mediaUrl }\n  externalMessageId: v.optional(v.string()),\n  status: v.union(\n    v.literal('received'),\n    v.literal('queued'),\n    v.literal('sent'),\n    v.literal('delivered'),\n    v.literal('read'),\n    v.literal('failed')\n  ),\n  metadata: v.optional(v.string()),      \u002F\u002F Channel-specific metadata (JSON)\n  createdAt: v.number(),\n})\n  .index('by_thread', ['threadId'])\n  .index('by_organization_and_channel', ['organizationId', 'channel'])\n  .index('by_contact', ['contactId'])\n",[40,687,688,701,711,727,736,768,787,804,817,831,841,855,868,881,894,907,921,926,943,953,958,980,1003],{"__ignoreMap":38},[43,689,690,692,695,698],{"class":45,"line":46},[43,691,681],{"class":53},[43,693,694],{"class":57},": ",[43,696,697],{"class":119},"defineTable",[43,699,700],{"class":57},"({\n",[43,702,703,706,708],{"class":45,"line":61},[43,704,705],{"class":57},"  organizationId: v.",[43,707,525],{"class":119},[43,709,710],{"class":57},"(),\n",[43,712,713,716,719,721,724],{"class":45,"line":68},[43,714,715],{"class":57},"  threadId: v.",[43,717,718],{"class":119},"id",[43,720,123],{"class":57},[43,722,723],{"class":78},"'conversationThreads'",[43,725,726],{"class":57},"),\n",[43,728,729,732,734],{"class":45,"line":103},[43,730,731],{"class":57},"  channel: v.",[43,733,525],{"class":119},[43,735,710],{"class":57},[43,737,738,741,744,747,750,752,755,758,760,762,765],{"class":45,"line":110},[43,739,740],{"class":57},"  direction: v.",[43,742,743],{"class":119},"union",[43,745,746],{"class":57},"(v.",[43,748,749],{"class":119},"literal",[43,751,123],{"class":57},[43,753,754],{"class":78},"'inbound'",[43,756,757],{"class":57},"), v.",[43,759,749],{"class":119},[43,761,123],{"class":57},[43,763,764],{"class":78},"'outbound'",[43,766,767],{"class":57},")),\n",[43,769,770,773,776,778,780,782,785],{"class":45,"line":116},[43,771,772],{"class":57},"  contactId: v.",[43,774,775],{"class":119},"optional",[43,777,746],{"class":57},[43,779,718],{"class":119},[43,781,123],{"class":57},[43,783,784],{"class":78},"'contacts'",[43,786,767],{"class":57},[43,788,789,792,794,796,798,801],{"class":45,"line":151},[43,790,791],{"class":57},"  memberId: v.",[43,793,775],{"class":119},[43,795,746],{"class":57},[43,797,525],{"class":119},[43,799,800],{"class":57},"()),     ",[43,802,803],{"class":64},"\u002F\u002F internal sender (BetterAuth user ID)\n",[43,805,806,809,811,814],{"class":45,"line":156},[43,807,808],{"class":57},"  content: v.",[43,810,525],{"class":119},[43,812,813],{"class":57},"(),                   ",[43,815,816],{"class":64},"\u002F\u002F JSON: { text, html, subject, mediaUrl }\n",[43,818,819,822,824,826,828],{"class":45,"line":162},[43,820,821],{"class":57},"  externalMessageId: v.",[43,823,775],{"class":119},[43,825,746],{"class":57},[43,827,525],{"class":119},[43,829,830],{"class":57},"()),\n",[43,832,833,836,838],{"class":45,"line":186},[43,834,835],{"class":57},"  status: v.",[43,837,743],{"class":119},[43,839,840],{"class":57},"(\n",[43,842,843,846,848,850,853],{"class":45,"line":191},[43,844,845],{"class":57},"    v.",[43,847,749],{"class":119},[43,849,123],{"class":57},[43,851,852],{"class":78},"'received'",[43,854,726],{"class":57},[43,856,857,859,861,863,866],{"class":45,"line":197},[43,858,845],{"class":57},[43,860,749],{"class":119},[43,862,123],{"class":57},[43,864,865],{"class":78},"'queued'",[43,867,726],{"class":57},[43,869,870,872,874,876,879],{"class":45,"line":226},[43,871,845],{"class":57},[43,873,749],{"class":119},[43,875,123],{"class":57},[43,877,878],{"class":78},"'sent'",[43,880,726],{"class":57},[43,882,883,885,887,889,892],{"class":45,"line":231},[43,884,845],{"class":57},[43,886,749],{"class":119},[43,888,123],{"class":57},[43,890,891],{"class":78},"'delivered'",[43,893,726],{"class":57},[43,895,896,898,900,902,905],{"class":45,"line":237},[43,897,845],{"class":57},[43,899,749],{"class":119},[43,901,123],{"class":57},[43,903,904],{"class":78},"'read'",[43,906,726],{"class":57},[43,908,909,911,913,915,918],{"class":45,"line":266},[43,910,845],{"class":57},[43,912,749],{"class":119},[43,914,123],{"class":57},[43,916,917],{"class":78},"'failed'",[43,919,920],{"class":57},")\n",[43,922,923],{"class":45,"line":271},[43,924,925],{"class":57},"  ),\n",[43,927,928,931,933,935,937,940],{"class":45,"line":277},[43,929,930],{"class":57},"  metadata: v.",[43,932,775],{"class":119},[43,934,746],{"class":57},[43,936,525],{"class":119},[43,938,939],{"class":57},"()),      ",[43,941,942],{"class":64},"\u002F\u002F Channel-specific metadata (JSON)\n",[43,944,945,948,951],{"class":45,"line":297},[43,946,947],{"class":57},"  createdAt: v.",[43,949,950],{"class":119},"number",[43,952,710],{"class":57},[43,954,955],{"class":45,"line":303},[43,956,957],{"class":57},"})\n",[43,959,960,963,966,968,971,974,977],{"class":45,"line":308},[43,961,962],{"class":57},"  .",[43,964,965],{"class":119},"index",[43,967,123],{"class":57},[43,969,970],{"class":78},"'by_thread'",[43,972,973],{"class":57},", [",[43,975,976],{"class":78},"'threadId'",[43,978,979],{"class":57},"])\n",[43,981,982,984,986,988,991,993,996,998,1001],{"class":45,"line":318},[43,983,962],{"class":57},[43,985,965],{"class":119},[43,987,123],{"class":57},[43,989,990],{"class":78},"'by_organization_and_channel'",[43,992,973],{"class":57},[43,994,995],{"class":78},"'organizationId'",[43,997,528],{"class":57},[43,999,1000],{"class":78},"'channel'",[43,1002,979],{"class":57},[43,1004,1005,1007,1009,1011,1014,1016,1019],{"class":45,"line":339},[43,1006,962],{"class":57},[43,1008,965],{"class":119},[43,1010,123],{"class":57},[43,1012,1013],{"class":78},"'by_contact'",[43,1015,973],{"class":57},[43,1017,1018],{"class":78},"'contactId'",[43,1020,979],{"class":57},[15,1022,1023,1024,1027,1028,1032],{},"The ",[40,1025,1026],{},"conversationThreads"," table (introduced in the ",[19,1029,1031],{"href":1030},"\u002Fvision\u002Fagent-pipeline","Agent Pipeline",") becomes the universal hub. A thread can contain email messages, SMS messages, chat messages, and webhook events — all in chronological order.",[555,1034,1036],{"id":1035},"channel-configuration","Channel configuration",[15,1038,1039],{},"Each organization configures which channels are active and how they connect:",[33,1041,1043],{"className":35,"code":1042,"language":37,"meta":38,"style":38},"channelConfigs: defineTable({\n  organizationId: v.string(),\n  channel: v.string(),\n  enabled: v.boolean(),\n  config: v.string(),  \u002F\u002F JSON: provider-specific (API keys, phone numbers, etc.)\n  createdAt: v.number(),\n  updatedAt: v.number(),\n})\n  .index('by_organization', ['organizationId'])\n  .index('by_organization_and_channel', ['organizationId', 'channel'])\n",[40,1044,1045,1056,1064,1072,1081,1094,1102,1111,1115,1132],{"__ignoreMap":38},[43,1046,1047,1050,1052,1054],{"class":45,"line":46},[43,1048,1049],{"class":53},"channelConfigs",[43,1051,694],{"class":57},[43,1053,697],{"class":119},[43,1055,700],{"class":57},[43,1057,1058,1060,1062],{"class":45,"line":61},[43,1059,705],{"class":57},[43,1061,525],{"class":119},[43,1063,710],{"class":57},[43,1065,1066,1068,1070],{"class":45,"line":68},[43,1067,731],{"class":57},[43,1069,525],{"class":119},[43,1071,710],{"class":57},[43,1073,1074,1077,1079],{"class":45,"line":103},[43,1075,1076],{"class":57},"  enabled: v.",[43,1078,261],{"class":119},[43,1080,710],{"class":57},[43,1082,1083,1086,1088,1091],{"class":45,"line":110},[43,1084,1085],{"class":57},"  config: v.",[43,1087,525],{"class":119},[43,1089,1090],{"class":57},"(),  ",[43,1092,1093],{"class":64},"\u002F\u002F JSON: provider-specific (API keys, phone numbers, etc.)\n",[43,1095,1096,1098,1100],{"class":45,"line":116},[43,1097,947],{"class":57},[43,1099,950],{"class":119},[43,1101,710],{"class":57},[43,1103,1104,1107,1109],{"class":45,"line":151},[43,1105,1106],{"class":57},"  updatedAt: v.",[43,1108,950],{"class":119},[43,1110,710],{"class":57},[43,1112,1113],{"class":45,"line":156},[43,1114,957],{"class":57},[43,1116,1117,1119,1121,1123,1126,1128,1130],{"class":45,"line":162},[43,1118,962],{"class":57},[43,1120,965],{"class":119},[43,1122,123],{"class":57},[43,1124,1125],{"class":78},"'by_organization'",[43,1127,973],{"class":57},[43,1129,995],{"class":78},[43,1131,979],{"class":57},[43,1133,1134,1136,1138,1140,1142,1144,1146,1148,1150],{"class":45,"line":186},[43,1135,962],{"class":57},[43,1137,965],{"class":119},[43,1139,123],{"class":57},[43,1141,990],{"class":78},[43,1143,973],{"class":57},[43,1145,995],{"class":78},[43,1147,528],{"class":57},[43,1149,1000],{"class":78},[43,1151,979],{"class":57},[15,1153,1154],{},"Channel configurations store encrypted credentials. For self-hosters, SMS and WhatsApp require API keys from the respective providers — these are the only external dependencies that cannot be self-hosted.",[555,1156,1158],{"id":1157},"adapter-health-monitoring","Adapter health monitoring",[15,1160,1161,1162,1165,1166,1168],{},"Each adapter reports its connection health via ",[40,1163,1164],{},"healthCheck()",". A Convex cron job polls adapter health every 5 minutes and updates ",[40,1167,1049],{}," with the latest status:",[33,1170,1172],{"className":35,"code":1171,"language":37,"meta":38,"style":38},"channelHealth: v.optional(v.object({\n  status: v.union(v.literal('healthy'), v.literal('degraded'), v.literal('down')),\n  lastCheckedAt: v.number(),\n  lastSuccessfulSend: v.optional(v.number()),\n  lastError: v.optional(v.string()),\n  rateLimitRemaining: v.optional(v.number()),\n})),\n",[40,1173,1174,1191,1226,1235,1248,1261,1274],{"__ignoreMap":38},[43,1175,1176,1179,1182,1184,1186,1189],{"class":45,"line":46},[43,1177,1178],{"class":53},"channelHealth",[43,1180,1181],{"class":57},": v.",[43,1183,775],{"class":119},[43,1185,746],{"class":57},[43,1187,1188],{"class":119},"object",[43,1190,700],{"class":57},[43,1192,1193,1195,1197,1199,1201,1203,1206,1208,1210,1212,1215,1217,1219,1221,1224],{"class":45,"line":61},[43,1194,835],{"class":57},[43,1196,743],{"class":119},[43,1198,746],{"class":57},[43,1200,749],{"class":119},[43,1202,123],{"class":57},[43,1204,1205],{"class":78},"'healthy'",[43,1207,757],{"class":57},[43,1209,749],{"class":119},[43,1211,123],{"class":57},[43,1213,1214],{"class":78},"'degraded'",[43,1216,757],{"class":57},[43,1218,749],{"class":119},[43,1220,123],{"class":57},[43,1222,1223],{"class":78},"'down'",[43,1225,767],{"class":57},[43,1227,1228,1231,1233],{"class":45,"line":68},[43,1229,1230],{"class":57},"  lastCheckedAt: v.",[43,1232,950],{"class":119},[43,1234,710],{"class":57},[43,1236,1237,1240,1242,1244,1246],{"class":45,"line":103},[43,1238,1239],{"class":57},"  lastSuccessfulSend: v.",[43,1241,775],{"class":119},[43,1243,746],{"class":57},[43,1245,950],{"class":119},[43,1247,830],{"class":57},[43,1249,1250,1253,1255,1257,1259],{"class":45,"line":110},[43,1251,1252],{"class":57},"  lastError: v.",[43,1254,775],{"class":119},[43,1256,746],{"class":57},[43,1258,525],{"class":119},[43,1260,830],{"class":57},[43,1262,1263,1266,1268,1270,1272],{"class":45,"line":116},[43,1264,1265],{"class":57},"  rateLimitRemaining: v.",[43,1267,775],{"class":119},[43,1269,746],{"class":57},[43,1271,950],{"class":119},[43,1273,830],{"class":57},[43,1275,1276],{"class":45,"line":151},[43,1277,1278],{"class":57},"})),\n",[15,1280,1281,1282,1285,1286,1289],{},"When an adapter reports ",[40,1283,1284],{},"degraded"," or ",[40,1287,1288],{},"down"," status:",[1291,1292,1293,1300],"ul",{},[1294,1295,1296,1299],"li",{},[590,1297,1298],{},"Degraded"," — outbound messages queue with exponential backoff instead of immediate send. The dashboard shows a warning.",[1294,1301,1302,1305],{},[590,1303,1304],{},"Down"," — outbound messages queue for later delivery. Inbound webhooks continue to accept and store messages (the provider is sending, even if we cannot send back). The dashboard shows an alert with the last error.",[15,1307,1308],{},"This prevents silent failures — if the Twilio API goes down, support agents see the degradation in their dashboard before customers report missing SMS replies.",[25,1310,1312],{"id":1311},"inbound-webhook-pattern","Inbound webhook pattern",[15,1314,1315],{},"Each channel's inbound messages arrive via HTTP webhook. The pattern is identical to the existing MTA and Resend webhook handlers:",[33,1317,1322],{"className":1318,"code":1320,"language":1321},[1319],"language-text","External provider (Twilio, Meta, etc.)\n  → POST \u002Fwebhooks\u002F{channel}\n  → Channel adapter validateSignature() verifies webhook authenticity\n  → Channel adapter parseInbound() normalizes the payload\n  → Store in unifiedMessages\n  → Link to conversationThread (or create new thread)\n  → Message coalescing check (debounce window for thread bursts)\n  → Feed into Agent Pipeline (if configured)\n","text",[40,1323,1320],{"__ignoreMap":38},[15,1325,1326,1327,75],{},"New routes added to ",[40,1328,1329],{},"apps\u002Fapi\u002Fconvex\u002Fhttp.ts",[33,1331,1333],{"className":35,"code":1332,"language":37,"meta":38,"style":38},"\u002F\u002F SMS inbound\nhttp.route({\n  path: '\u002Fwebhooks\u002Fsms',\n  method: 'POST',\n  handler: handleSmsWebhook,\n})\n\n\u002F\u002F WhatsApp inbound\nhttp.route({\n  path: '\u002Fwebhooks\u002Fwhatsapp',\n  method: 'POST',\n  handler: handleWhatsAppWebhook,\n})\n\n\u002F\u002F Generic webhook (for custom integrations)\nhttp.route({\n  pathPrefix: '\u002Fwebhooks\u002Fcustom\u002F',\n  method: 'POST',\n  handler: handleCustomWebhook,\n})\n",[40,1334,1335,1340,1350,1361,1371,1376,1380,1384,1389,1397,1406,1414,1419,1423,1427,1432,1440,1450,1458,1463],{"__ignoreMap":38},[43,1336,1337],{"class":45,"line":46},[43,1338,1339],{"class":64},"\u002F\u002F SMS inbound\n",[43,1341,1342,1345,1348],{"class":45,"line":61},[43,1343,1344],{"class":57},"http.",[43,1346,1347],{"class":119},"route",[43,1349,700],{"class":57},[43,1351,1352,1355,1358],{"class":45,"line":68},[43,1353,1354],{"class":57},"  path: ",[43,1356,1357],{"class":78},"'\u002Fwebhooks\u002Fsms'",[43,1359,1360],{"class":57},",\n",[43,1362,1363,1366,1369],{"class":45,"line":103},[43,1364,1365],{"class":57},"  method: ",[43,1367,1368],{"class":78},"'POST'",[43,1370,1360],{"class":57},[43,1372,1373],{"class":45,"line":110},[43,1374,1375],{"class":57},"  handler: handleSmsWebhook,\n",[43,1377,1378],{"class":45,"line":116},[43,1379,957],{"class":57},[43,1381,1382],{"class":45,"line":151},[43,1383,107],{"emptyLinePlaceholder":106},[43,1385,1386],{"class":45,"line":156},[43,1387,1388],{"class":64},"\u002F\u002F WhatsApp inbound\n",[43,1390,1391,1393,1395],{"class":45,"line":162},[43,1392,1344],{"class":57},[43,1394,1347],{"class":119},[43,1396,700],{"class":57},[43,1398,1399,1401,1404],{"class":45,"line":186},[43,1400,1354],{"class":57},[43,1402,1403],{"class":78},"'\u002Fwebhooks\u002Fwhatsapp'",[43,1405,1360],{"class":57},[43,1407,1408,1410,1412],{"class":45,"line":191},[43,1409,1365],{"class":57},[43,1411,1368],{"class":78},[43,1413,1360],{"class":57},[43,1415,1416],{"class":45,"line":197},[43,1417,1418],{"class":57},"  handler: handleWhatsAppWebhook,\n",[43,1420,1421],{"class":45,"line":226},[43,1422,957],{"class":57},[43,1424,1425],{"class":45,"line":231},[43,1426,107],{"emptyLinePlaceholder":106},[43,1428,1429],{"class":45,"line":237},[43,1430,1431],{"class":64},"\u002F\u002F Generic webhook (for custom integrations)\n",[43,1433,1434,1436,1438],{"class":45,"line":266},[43,1435,1344],{"class":57},[43,1437,1347],{"class":119},[43,1439,700],{"class":57},[43,1441,1442,1445,1448],{"class":45,"line":271},[43,1443,1444],{"class":57},"  pathPrefix: ",[43,1446,1447],{"class":78},"'\u002Fwebhooks\u002Fcustom\u002F'",[43,1449,1360],{"class":57},[43,1451,1452,1454,1456],{"class":45,"line":277},[43,1453,1365],{"class":57},[43,1455,1368],{"class":78},[43,1457,1360],{"class":57},[43,1459,1460],{"class":45,"line":297},[43,1461,1462],{"class":57},"  handler: handleCustomWebhook,\n",[43,1464,1465],{"class":45,"line":303},[43,1466,957],{"class":57},[25,1468,1470],{"id":1469},"crm-hub","CRM Hub",[15,1472,1473,1474,1477],{},"The CRM hub extends the existing ",[40,1475,1476],{},"contacts"," table with unified identity management and relationship intelligence.",[555,1479,1481],{"id":1480},"contact-identity-unification","Contact identity unification",[15,1483,1484],{},"The same person often communicates through multiple channels — work email, personal email, phone, WhatsApp. The CRM unifies these into a single contact profile:",[33,1486,1488],{"className":35,"code":1487,"language":37,"meta":38,"style":38},"contactIdentities: defineTable({\n  contactId: v.id('contacts'),\n  channel: v.string(),         \u002F\u002F 'email', 'phone', 'whatsapp', 'twitter', etc.\n  identifier: v.string(),      \u002F\u002F email address, phone number, handle\n  isPrimary: v.boolean(),\n  verifiedAt: v.optional(v.number()),\n  createdAt: v.number(),\n})\n  .index('by_contact', ['contactId'])\n  .index('by_identifier', ['channel', 'identifier'])\n",[40,1489,1490,1501,1513,1525,1538,1547,1560,1568,1572,1588],{"__ignoreMap":38},[43,1491,1492,1495,1497,1499],{"class":45,"line":46},[43,1493,1494],{"class":53},"contactIdentities",[43,1496,694],{"class":57},[43,1498,697],{"class":119},[43,1500,700],{"class":57},[43,1502,1503,1505,1507,1509,1511],{"class":45,"line":61},[43,1504,772],{"class":57},[43,1506,718],{"class":119},[43,1508,123],{"class":57},[43,1510,784],{"class":78},[43,1512,726],{"class":57},[43,1514,1515,1517,1519,1522],{"class":45,"line":68},[43,1516,731],{"class":57},[43,1518,525],{"class":119},[43,1520,1521],{"class":57},"(),         ",[43,1523,1524],{"class":64},"\u002F\u002F 'email', 'phone', 'whatsapp', 'twitter', etc.\n",[43,1526,1527,1530,1532,1535],{"class":45,"line":103},[43,1528,1529],{"class":57},"  identifier: v.",[43,1531,525],{"class":119},[43,1533,1534],{"class":57},"(),      ",[43,1536,1537],{"class":64},"\u002F\u002F email address, phone number, handle\n",[43,1539,1540,1543,1545],{"class":45,"line":110},[43,1541,1542],{"class":57},"  isPrimary: v.",[43,1544,261],{"class":119},[43,1546,710],{"class":57},[43,1548,1549,1552,1554,1556,1558],{"class":45,"line":116},[43,1550,1551],{"class":57},"  verifiedAt: v.",[43,1553,775],{"class":119},[43,1555,746],{"class":57},[43,1557,950],{"class":119},[43,1559,830],{"class":57},[43,1561,1562,1564,1566],{"class":45,"line":151},[43,1563,947],{"class":57},[43,1565,950],{"class":119},[43,1567,710],{"class":57},[43,1569,1570],{"class":45,"line":156},[43,1571,957],{"class":57},[43,1573,1574,1576,1578,1580,1582,1584,1586],{"class":45,"line":162},[43,1575,962],{"class":57},[43,1577,965],{"class":119},[43,1579,123],{"class":57},[43,1581,1013],{"class":78},[43,1583,973],{"class":57},[43,1585,1018],{"class":78},[43,1587,979],{"class":57},[43,1589,1590,1592,1594,1596,1599,1601,1603,1605,1608],{"class":45,"line":186},[43,1591,962],{"class":57},[43,1593,965],{"class":119},[43,1595,123],{"class":57},[43,1597,1598],{"class":78},"'by_identifier'",[43,1600,973],{"class":57},[43,1602,1000],{"class":78},[43,1604,528],{"class":57},[43,1606,1607],{"class":78},"'identifier'",[43,1609,979],{"class":57},[15,1611,1612,1613,1615],{},"When an inbound message arrives, the pipeline checks ",[40,1614,1494],{}," for a matching identifier:",[1617,1618,1619,1625,1631],"ol",{},[1294,1620,1621,1624],{},[590,1622,1623],{},"Match found"," → link to existing contact, update thread",[1294,1626,1627,1630],{},[590,1628,1629],{},"No match"," → create a new contact and identity",[1294,1632,1633,1636],{},[590,1634,1635],{},"Suggested merge"," → when signals suggest two contacts are the same person (email signature contains a phone number already linked to another contact), surface a merge suggestion in the UI",[555,1638,1640],{"id":1639},"relationship-intelligence","Relationship intelligence",[15,1642,1643],{},"The Knowledge Graph feeds relationship insights into the CRM:",[33,1645,1647],{"className":35,"code":1646,"language":37,"meta":38,"style":38},"contactRelationships: defineTable({\n  organizationId: v.string(),\n  fromContactId: v.id('contacts'),\n  toContactId: v.id('contacts'),\n  relationship: v.string(),    \u002F\u002F \"manager_of\", \"colleague\", \"reports_to\", etc.\n  confidence: v.number(),\n  source: v.union(v.literal('manual'), v.literal('agent_extracted')),\n  createdAt: v.number(),\n})\n  .index('by_from', ['fromContactId'])\n  .index('by_to', ['toContactId'])\n  .index('by_organization', ['organizationId'])\n",[40,1648,1649,1660,1668,1681,1694,1707,1716,1743,1751,1755,1773,1791],{"__ignoreMap":38},[43,1650,1651,1654,1656,1658],{"class":45,"line":46},[43,1652,1653],{"class":53},"contactRelationships",[43,1655,694],{"class":57},[43,1657,697],{"class":119},[43,1659,700],{"class":57},[43,1661,1662,1664,1666],{"class":45,"line":61},[43,1663,705],{"class":57},[43,1665,525],{"class":119},[43,1667,710],{"class":57},[43,1669,1670,1673,1675,1677,1679],{"class":45,"line":68},[43,1671,1672],{"class":57},"  fromContactId: v.",[43,1674,718],{"class":119},[43,1676,123],{"class":57},[43,1678,784],{"class":78},[43,1680,726],{"class":57},[43,1682,1683,1686,1688,1690,1692],{"class":45,"line":103},[43,1684,1685],{"class":57},"  toContactId: v.",[43,1687,718],{"class":119},[43,1689,123],{"class":57},[43,1691,784],{"class":78},[43,1693,726],{"class":57},[43,1695,1696,1699,1701,1704],{"class":45,"line":110},[43,1697,1698],{"class":57},"  relationship: v.",[43,1700,525],{"class":119},[43,1702,1703],{"class":57},"(),    ",[43,1705,1706],{"class":64},"\u002F\u002F \"manager_of\", \"colleague\", \"reports_to\", etc.\n",[43,1708,1709,1712,1714],{"class":45,"line":116},[43,1710,1711],{"class":57},"  confidence: v.",[43,1713,950],{"class":119},[43,1715,710],{"class":57},[43,1717,1718,1721,1723,1725,1727,1729,1732,1734,1736,1738,1741],{"class":45,"line":151},[43,1719,1720],{"class":57},"  source: v.",[43,1722,743],{"class":119},[43,1724,746],{"class":57},[43,1726,749],{"class":119},[43,1728,123],{"class":57},[43,1730,1731],{"class":78},"'manual'",[43,1733,757],{"class":57},[43,1735,749],{"class":119},[43,1737,123],{"class":57},[43,1739,1740],{"class":78},"'agent_extracted'",[43,1742,767],{"class":57},[43,1744,1745,1747,1749],{"class":45,"line":156},[43,1746,947],{"class":57},[43,1748,950],{"class":119},[43,1750,710],{"class":57},[43,1752,1753],{"class":45,"line":162},[43,1754,957],{"class":57},[43,1756,1757,1759,1761,1763,1766,1768,1771],{"class":45,"line":186},[43,1758,962],{"class":57},[43,1760,965],{"class":119},[43,1762,123],{"class":57},[43,1764,1765],{"class":78},"'by_from'",[43,1767,973],{"class":57},[43,1769,1770],{"class":78},"'fromContactId'",[43,1772,979],{"class":57},[43,1774,1775,1777,1779,1781,1784,1786,1789],{"class":45,"line":191},[43,1776,962],{"class":57},[43,1778,965],{"class":119},[43,1780,123],{"class":57},[43,1782,1783],{"class":78},"'by_to'",[43,1785,973],{"class":57},[43,1787,1788],{"class":78},"'toContactId'",[43,1790,979],{"class":57},[43,1792,1793,1795,1797,1799,1801,1803,1805],{"class":45,"line":197},[43,1794,962],{"class":57},[43,1796,965],{"class":119},[43,1798,123],{"class":57},[43,1800,1125],{"class":78},[43,1802,973],{"class":57},[43,1804,995],{"class":78},[43,1806,979],{"class":57},[15,1808,1809],{},"The CRM view for each contact shows:",[1291,1811,1812,1818,1824,1830,1836,1842],{},[1294,1813,1814,1817],{},[590,1815,1816],{},"Unified timeline"," — all messages across all channels in chronological order",[1294,1819,1820,1823],{},[590,1821,1822],{},"Knowledge summary"," — key facts, preferences, and goals from the Knowledge Graph",[1294,1825,1826,1829],{},[590,1827,1828],{},"Relationship map"," — connections to other contacts (colleagues, reports, etc.)",[1294,1831,1832,1835],{},[590,1833,1834],{},"Sentiment trend"," — how the contact's sentiment has evolved over time",[1294,1837,1838,1841],{},[590,1839,1840],{},"Outstanding commitments"," — promises made in conversations (\"you said you'd send the proposal by Tuesday\")",[1294,1843,1844,1847],{},[590,1845,1846],{},"Communication preferences"," — preferred channel, response patterns, active hours",[555,1849,1851],{"id":1850},"communication-native-updates","Communication-native updates",[15,1853,1854],{},"Traditional CRMs require manual data entry. In Owlat, the CRM builds itself from actual communication:",[1291,1856,1857,1860,1863,1866],{},[1294,1858,1859],{},"When you email an investor → the interaction is logged automatically",[1294,1861,1862],{},"When a customer's sentiment shifts negative → the relationship health score updates",[1294,1864,1865],{},"When a deal is discussed in a thread → the pipeline status reflects the conversation",[1294,1867,1868],{},"When a contact changes jobs (detected from email signature changes) → the profile updates",[15,1870,1871],{},"All of this happens through the Knowledge Graph extraction pipeline — the CRM is a view on the knowledge graph, not a separate data store.",[1873,1874,1875],"style",{},"html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}html pre.shiki code .stnAF, html code.shiki .stnAF{--shiki-default:#E36209;--shiki-dark:#F69D50}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":38,"searchDepth":61,"depth":61,"links":1877},[1878,1881,1885,1886],{"id":27,"depth":61,"text":28,"children":1879},[1880],{"id":557,"depth":68,"text":558},{"id":674,"depth":61,"text":675,"children":1882},[1883,1884],{"id":1035,"depth":68,"text":1036},{"id":1157,"depth":68,"text":1158},{"id":1311,"depth":61,"text":1312},{"id":1469,"depth":61,"text":1470,"children":1887},[1888,1889,1890],{"id":1480,"depth":68,"text":1481},{"id":1639,"depth":68,"text":1640},{"id":1850,"depth":68,"text":1851},"Technical architecture for channel adapters, unified messaging, contact identity unification, and the CRM hub.","md",{},"\u002Fvision\u002Fmulti-channel",{"title":5,"description":1891},"5.vision\u002F4.multi-channel","2kQNZaPB4jgp0XXnM0NfrmzEwndbcEWqpiyaPM_BPDI",[1899,1903],{"title":1900,"path":1901,"stem":1902,"children":-1},"Knowledge Graph","\u002Fvision\u002Fknowledge-graph","5.vision\u002F3.knowledge-graph",{"title":1904,"path":1905,"stem":1906,"children":-1},"Semantic File System","\u002Fvision\u002Ffile-system","5.vision\u002F5.file-system",1774391045843]