Skip to content

B2B Multi-Channel Architecture Plan

Status: Future Roadmap (Not Yet Implemented) Created: January 2026 Last Updated: January 2026 Priority: When B2B use cases become a priority

This document outlines the architecture changes needed to support B2B white-label AI personalities via official messaging channels (SMS, WhatsApp, Web Chat) instead of the current iMessage-only approach.


Table of Contents

  1. Executive Summary
  2. Why This Matters
  3. Risk Analysis
  4. Target Architecture
  5. Implementation Phases
  6. Database Migrations
  7. Code Examples
  8. Testing Strategy
  9. Sources & Research

Executive Summary

The Problem

Our current iMessage integration uses an unofficial method (Mac Mini + AppleScript relay) that: - Violates Apple ToS - Same gray zone as Poke, Sendblue, LoopMessage - Cannot scale to B2B - Each business would need their own Mac + Apple ID - Has no SLA guarantees - Apple could break it with any macOS update

The Solution

Build a multi-channel abstraction layer that: 1. Keeps iMessage for B2C (acceptable risk at small scale) 2. Adds official channels (SMS, WhatsApp, Web Chat) for B2B via Twilio 3. Enables organizations to create white-label personas with full prompt control 4. Isolates data between organizations (memory, conversations, billing)

Key Decisions Made

Decision Choice Rationale
Phone provisioning We provision for B2B customers Simpler onboarding, we control quality
Web Chat timeline Include in Phase 2 Zero-cost testing channel
Persona customization Full prompt control with safety rails Maximum flexibility for businesses

Effort Estimate

  • Sequential execution: 11-16 weeks
  • Parallel execution: 8-10 weeks
  • Phases: 6 distinct phases, can be deployed incrementally

Why This Matters

B2B Use Cases

Businesses want to deploy AI personalities via text messaging:

Business Type Use Case Example
Airlines Platinum member support United proactive flight updates
Home Services Coordination & scheduling Bathroom remodeler appointment booking
Hospitality Guest concierge Hotel recommendations & reservations
Healthcare Appointment reminders Clinic follow-up coordination

Why iMessage Won't Work for B2B

  1. No programmatic identity provisioning - Can't spin up new iMessage accounts via API
  2. Apple ToS violations at scale - Enterprise customers need compliance guarantees
  3. No SLAs - Apple could shut down the approach with any update
  4. Single point of failure - Mac Mini dependency

What B2B Customers Need

Requirement iMessage (Current) Official Channels (Proposed)
Proactive messaging ✅ Works ✅ SMS/WhatsApp allow with opt-in
Compliance (SOC2, HIPAA) ❌ Gray zone ✅ Twilio has BAA options
SLA guarantees ❌ None ✅ 99.95% uptime SLAs
Programmatic setup ❌ Manual Mac setup ✅ API-driven
White-label branding ❌ Fixed Sage/Echo ✅ Full customization

Risk Analysis

Apple Platform Risk: CRITICAL

Current approach violates Apple ToS:

"If vendors do not abide by Apple permissions and install unauthorized software to impermissibly capture information, this poses a legal and compliance violation that will inevitably be shut down." — Global Relay Compliance Analysis

How competitors handle this:

Service Method Status
Poke Mac farm (likely) $100M valuation, gray zone
Beeper Mini Protocol reverse-engineering Shut down by Apple
Sendblue "iPhone in the cloud" Commercial API, gray zone
Our current setup Single Mac Mini Small scale, gray zone

Apple's official path (Messages for Business) has deal-breaker restrictions: - Customer-initiated only (no proactive messaging) - Human agent escalation mandatory - Requires MSP partnership and Apple approval

Technical Risk: MODERATE

Our architecture is well-positioned for multi-tenancy:

What already exists: - Room model with memberships and roles - AssistantInstance for per-room persona config - Memory namespace isolation in Supermemory - MiniApp framework for custom workflows

What's missing: - Organization model (~2 weeks) - Org-scoped personas (~2 weeks) - Channel abstraction layer (~3 weeks) - Auth context with org_id (~1 week)

Product Risk: MODERATE-HIGH

  • Competing with Zendesk, Intercom, Drift (established B2B chat)
  • Our differentiator (persona + memory) is portable to any channel
  • iMessage limits TAM to ~60% of US smartphones

Target Architecture

Current State

iMessage → Mac Mini → POST /edge/message → MessageHandler → TwoStageHandler
                                          WebSocket EdgeCommand → Mac Mini → iMessage

Target State

┌─────────────────────────────────────────────────────────────────────────────┐
│                              INBOUND CHANNELS                               │
├─────────────┬─────────────┬─────────────┬─────────────┬─────────────────────┤
│  iMessage   │    SMS      │  WhatsApp   │    RCS      │     Web Chat        │
│  (Mac Mini) │  (Twilio)   │  (Twilio)   │  (Google)   │   (WebSocket)       │
└──────┬──────┴──────┬──────┴──────┬──────┴──────┬──────┴──────────┬──────────┘
       │             │             │             │                  │
       ▼             ▼             ▼             ▼                  ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                        CHANNEL ADAPTER LAYER (NEW)                          │
│   ChannelRouter.route_inbound() → ChannelMessage (normalized)              │
└─────────────────────────────────┬───────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│                         ORCHESTRATOR (existing)                             │
│   OrchestratorRequest → MessageHandler → TwoStageHandler → Response        │
│                                                                             │
│   + AuthContext (org_id, org_role)                                         │
│   + OrgPersonaResolver (org-specific personas)                             │
│   + Supermemory (org-isolated namespaces)                                  │
└─────────────────────────────────┬───────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│                        CHANNEL DELIVERY LAYER (NEW)                         │
│   ChannelRouter.send_response() → Channel-specific delivery                │
└──────┬──────┬──────┬──────┬──────┬──────────────────────────────────────────┘
       │      │      │      │      │
       ▼      ▼      ▼      ▼      ▼
   iMessage  SMS  WhatsApp  RCS   Web

Key Design Principles

  1. Channel-agnostic orchestrator - Core logic doesn't know about channels
  2. Pluggable adapters - Easy to add new channels
  3. Org isolation by default - Memory, personas, billing all org-scoped
  4. Safety rails on customization - Full prompt control wrapped with mandatory safety rules

Implementation Phases

Phase 1: Channel Abstraction Layer (2-3 weeks)

Goal: Create pluggable adapter system without breaking iMessage.

New files: - app/channels/base.py - ChannelAdapter, ChannelMessage, ChannelCapabilities - app/channels/router.py - ChannelRouter - app/channels/adapters/imessage.py - Extract from edge_routes.py

Modified files: - app/api/edge_routes.py - Use ChannelRouter - app/models/schemas.py - Add channel to metadata

Verification: - Existing iMessage flow unchanged - New channel field in request metadata - Unit tests for IMessageAdapter


Phase 2: SMS, WhatsApp & Web Chat (2-3 weeks)

Goal: Add official channels via Twilio + WebSocket.

New files: - app/channels/adapters/twilio_sms.py - app/channels/adapters/twilio_whatsapp.py - app/channels/adapters/web_chat.py - app/api/channel_routes.py - Twilio webhook - app/api/webchat_routes.py - WebSocket endpoint

Config changes:

TWILIO_MESSAGING_SERVICE_SID: Optional[str] = None
TWILIO_WHATSAPP_NUMBER: Optional[str] = None

Verification: - Send SMS → verify response - Send WhatsApp → verify response - Open web chat → verify WebSocket works


Phase 3: Organization Data Model (2-3 weeks)

Goal: Add multi-tenant organization support.

New tables: - organizations - Name, slug, tier, billing, limits - organization_memberships - User ↔ Org with roles - org_personas - White-label personas per org

Modified tables: - users - Add default_organization_id - rooms - Add organization_id

New files: - app/services/organization_service.py - app/api/organization_routes.py

Verification: - Create org via API - Add/remove members - Org appears in user's list


Phase 4: Org-Scoped Personas (2-3 weeks)

Goal: Full prompt control for business personas.

Modified files: - app/services/persona_resolver.py - Check org personas first - app/persona/engine.py - Apply org customizations with safety rails

New files: - app/services/org_persona_service.py - app/api/org_persona_routes.py

Persona customization options:

class OrgPersonaCreate(BaseModel):
    name: str  # "Aria", "Max"
    base_persona_id: str = "sage"

    # Full prompt control
    custom_system_prompt: Optional[str]  # Replace base entirely
    system_prompt_additions: Optional[str]  # Append to base

    # Safety & Legal
    prohibited_topics: Optional[List[str]]
    required_disclaimers: Optional[List[str]]

    # Voice/tone
    formality_level: int = 50  # 0-100
    humor_level: int = 50
    emoji_usage: str = "minimal"

Verification: - Create custom persona - Test via sandbox endpoint - Verify prohibited topics refused - Verify safety rails applied


Phase 5: Memory Isolation & Auth Context (1-2 weeks)

Goal: Complete data isolation between organizations.

Modified files: - app/memory/supermemory_service.py - Add org_id to namespace - app/dependencies.py - Add AuthContext with org - app/identity/session_manager.py - JWT includes org_id

Namespace strategy:

# Without org: user_{phone}_{persona_id}
# With org: org_{org_id}_user_{phone}_{persona_id}

Verification: - Org A memories not visible to Org B - JWT contains org context - API routes extract org correctly


Phase 6: Billing & Usage Metering (2 weeks)

Goal: Track usage and enforce limits per org.

New files: - app/services/usage_service.py - app/api/billing_routes.py

New table:

CREATE TABLE org_usage (
    organization_id UUID,
    period VARCHAR(7),  -- YYYY-MM
    channel VARCHAR(20),
    message_count INTEGER
);

Verification: - Usage increments on messages - 429 when limits exceeded - Stripe webhook updates status


Database Migrations

Main Migration: 20260115000000_add_organizations.sql

-- Organizations table
CREATE TABLE organizations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL,
    tier VARCHAR(20) DEFAULT 'smb',

    -- Branding
    logo_url VARCHAR(500),
    primary_color VARCHAR(7),

    -- Enterprise features
    sso_enabled BOOLEAN DEFAULT FALSE,
    sso_provider VARCHAR(50),
    sso_config JSONB,

    -- Billing
    stripe_customer_id VARCHAR(100),
    subscription_status VARCHAR(20) DEFAULT 'trial',
    subscription_plan VARCHAR(50),

    -- Limits
    max_users INTEGER DEFAULT 10,
    max_personas INTEGER DEFAULT 3,
    max_messages_per_month INTEGER DEFAULT 10000,

    status VARCHAR(20) DEFAULT 'active',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ
);

-- Organization memberships
CREATE TABLE organization_memberships (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    organization_id UUID NOT NULL REFERENCES organizations(id),
    user_id UUID NOT NULL REFERENCES users(id),
    role VARCHAR(20) DEFAULT 'member',
    invited_by UUID REFERENCES users(id),
    status VARCHAR(20) DEFAULT 'active',
    joined_at TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(organization_id, user_id)
);

-- Organization personas
CREATE TABLE org_personas (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    organization_id UUID NOT NULL REFERENCES organizations(id),
    name VARCHAR(100) NOT NULL,
    display_name VARCHAR(100),
    phone_number VARCHAR(20),
    base_persona_id VARCHAR(50) DEFAULT 'sage',

    -- Full prompt control
    custom_system_prompt TEXT,
    system_prompt_additions TEXT,
    personality_overrides JSONB,
    prohibited_topics JSONB,
    required_disclaimers JSONB,
    enabled_capabilities JSONB,

    -- Voice/tone
    formality_level INTEGER DEFAULT 50,
    humor_level INTEGER DEFAULT 50,
    emoji_usage VARCHAR(20) DEFAULT 'minimal',

    enabled_channels JSONB DEFAULT '["sms", "whatsapp", "web"]',
    status VARCHAR(20) DEFAULT 'active',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(organization_id, phone_number)
);

-- Extend existing tables
ALTER TABLE users ADD COLUMN default_organization_id UUID REFERENCES organizations(id);
ALTER TABLE rooms ADD COLUMN organization_id UUID REFERENCES organizations(id);

-- Indexes
CREATE INDEX idx_org_slug ON organizations(slug);
CREATE INDEX idx_org_membership ON organization_memberships(organization_id, status);
CREATE INDEX idx_user_orgs ON organization_memberships(user_id, status);
CREATE INDEX idx_org_personas ON org_personas(organization_id, status);

Code Examples

Channel Adapter Base Class

# app/channels/base.py
from abc import ABC, abstractmethod
from typing import Optional, List, Dict
from datetime import datetime
from pydantic import BaseModel

class ChannelMessage(BaseModel):
    """Normalized message format across all channels."""
    channel: str  # "imessage", "sms", "whatsapp", "web"
    thread_id: str
    sender_id: str
    sender_phone: Optional[str] = None
    text: str
    timestamp: datetime
    is_group: bool = False
    participants: List[str] = []
    attachments: Optional[List[Dict]] = None
    raw_payload: Optional[Dict] = None

class ChannelCapabilities(BaseModel):
    supports_reactions: bool = False
    supports_typing_indicator: bool = False
    supports_read_receipts: bool = False
    supports_rich_messages: bool = False
    supports_multi_bubble: bool = False
    max_message_length: int = 4096

class ChannelAdapter(ABC):
    @property
    @abstractmethod
    def channel_name(self) -> str:
        pass

    @property
    @abstractmethod
    def capabilities(self) -> ChannelCapabilities:
        pass

    @abstractmethod
    async def parse_inbound(self, raw_payload: Dict) -> Optional[ChannelMessage]:
        pass

    @abstractmethod
    async def send_message(self, response: ChannelResponse) -> bool:
        pass

    @abstractmethod
    def should_skip_message(self, message: ChannelMessage) -> bool:
        pass

Twilio Webhook

# app/api/channel_routes.py
@router.post("/twilio/webhook")
async def twilio_webhook(request: Request):
    """Unified Twilio webhook for SMS and WhatsApp."""
    form_data = await request.form()
    payload = dict(form_data)

    # Detect channel
    channel = "whatsapp" if payload.get("From", "").startswith("whatsapp:") else "sms"

    # Resolve org from Twilio number
    to_number = payload.get("To", "").replace("whatsapp:", "")
    org_id = await get_org_by_phone(to_number)

    # Route through channel adapter
    router = get_channel_router()
    orchestrator_request = await router.route_inbound(channel, payload, org_id=org_id)

    response = await process_message(orchestrator_request)

    return Response(
        content=f'<Response><Message>{response.reply_text}</Message></Response>',
        media_type="application/xml"
    )

Org Persona with Safety Rails

# app/persona/engine.py
def build_system_prompt(
    self,
    persona_id: str,
    org_persona: Optional[OrgPersona] = None,
    ...
) -> str:
    """Build system prompt with full org customizations."""
    base_prompt = self._load_base_persona(persona_id)

    if org_persona:
        if org_persona.custom_system_prompt:
            # Full replacement with safety rails
            base_prompt = self._wrap_with_safety_rails(org_persona.custom_system_prompt)
        else:
            # Additive mode
            if org_persona.personality_overrides:
                base_prompt = self._apply_overrides(base_prompt, org_persona.personality_overrides)
            if org_persona.system_prompt_additions:
                base_prompt += f"\n\n{org_persona.system_prompt_additions}"

        # Always apply safety constraints
        if org_persona.prohibited_topics:
            base_prompt += self._format_prohibited_topics(org_persona.prohibited_topics)
        if org_persona.required_disclaimers:
            base_prompt += self._format_disclaimers(org_persona.required_disclaimers)

    return base_prompt

def _wrap_with_safety_rails(self, custom_prompt: str) -> str:
    """Wrap custom prompt with non-negotiable safety rules."""
    return f"""
{self._load_safety_rules()}

--- CUSTOM PERSONA INSTRUCTIONS ---
{custom_prompt}
--- END CUSTOM INSTRUCTIONS ---

{self._load_mandatory_behaviors()}
"""

Testing Strategy

Unit Tests

Test File Coverage
tests/test_channel_adapters.py Adapter parsing, sending, skip logic
tests/test_organization_service.py Org CRUD, membership management
tests/test_memory_isolation.py Cross-org memory isolation
tests/test_org_persona.py Persona customization, safety rails

Integration Tests

  1. Send message via each channel → verify response
  2. Create org persona → verify custom behavior
  3. Cross-org memory isolation → verify no leakage
  4. Usage limits → verify 429 on exceeded

End-to-End Test Flow

  1. Create organization via API
  2. Add custom persona with prohibited topics
  3. Configure Twilio webhook
  4. Send SMS to org's number
  5. Verify custom persona responds
  6. Verify prohibited topic refused
  7. Check usage tracking incremented

Rollback Strategy

Each phase can be independently rolled back:

Phase Rollback Method
1 Revert edge_routes.py changes
2 Disable Twilio webhook route
3-5 Feature flag ENABLE_ORGANIZATIONS=false
6 Disable usage tracking

Timeline Summary

Phase Scope Duration
1 Channel Abstraction 2-3 weeks
2 SMS, WhatsApp, Web Chat 2-3 weeks
3 Organization Data Model 2-3 weeks
4 Org Personas (Full Control) 2-3 weeks
5 Memory Isolation & Auth 1-2 weeks
6 Billing & Metering 2 weeks
Total 11-16 weeks
  • Track A: Phases 1-2 (channels) - 4-6 weeks
  • Track B: Phases 3-5 (multi-tenancy) - 5-8 weeks, starts after Phase 1
  • Track C: Phase 6 (billing) - 2 weeks, after Phase 3

With parallel execution: ~8-10 weeks total


Critical Files Reference

File Phase Changes
app/channels/base.py 1 NEW
app/channels/router.py 1 NEW
app/channels/adapters/imessage.py 1 NEW (extract from edge_routes)
app/channels/adapters/twilio_sms.py 2 NEW
app/channels/adapters/twilio_whatsapp.py 2 NEW
app/channels/adapters/web_chat.py 2 NEW
app/api/channel_routes.py 2 NEW
app/api/webchat_routes.py 2 NEW
app/api/edge_routes.py 1 MODIFY
app/models/database.py 3 MODIFY
app/services/organization_service.py 3 NEW
app/services/org_persona_service.py 4 NEW
app/services/persona_resolver.py 4 MODIFY
app/persona/engine.py 4 MODIFY
app/memory/supermemory_service.py 5 MODIFY
app/dependencies.py 5 MODIFY
app/identity/session_manager.py 5 MODIFY

Sources & Research

Apple Platform Risk

Alternative Channels

Competitor Analysis


Next Steps When Ready to Implement

  1. Validate B2B demand - Talk to potential customers (bathroom remodelers, airlines)
  2. Choose CPaaS provider - Twilio vs Vonage vs MessageBird
  3. Start with Phase 1 - Channel abstraction is non-breaking
  4. Set up Twilio account - Get messaging service SID and numbers
  5. Prototype web chat - Zero-cost way to test B2B flow

This document should be updated as the architecture evolves or new requirements emerge.