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¶
- Executive Summary
- Why This Matters
- Risk Analysis
- Target Architecture
- Implementation Phases
- Database Migrations
- Code Examples
- Testing Strategy
- 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¶
- No programmatic identity provisioning - Can't spin up new iMessage accounts via API
- Apple ToS violations at scale - Enterprise customers need compliance guarantees
- No SLAs - Apple could shut down the approach with any update
- 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¶
- Channel-agnostic orchestrator - Core logic doesn't know about channels
- Pluggable adapters - Easy to add new channels
- Org isolation by default - Memory, personas, billing all org-scoped
- 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:
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:
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¶
- Send message via each channel → verify response
- Create org persona → verify custom behavior
- Cross-org memory isolation → verify no leakage
- Usage limits → verify 429 on exceeded
End-to-End Test Flow¶
- Create organization via API
- Add custom persona with prohibited topics
- Configure Twilio webhook
- Send SMS to org's number
- Verify custom persona responds
- Verify prohibited topic refused
- 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 |
Parallel Execution (Recommended)¶
- 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¶
- Apple Messages for Business Guide 2025 (Zendesk)
- Apple Messages for Business FAQ
- Apple MSP Onboarding Requirements
- iMessage Compliance Risks
- Is iMessage HIPAA Compliant?
- Apple Messaging Capture Compliance (Global Relay)
Alternative Channels¶
Competitor Analysis¶
- Poke $15M Funding Announcement
- Beeper Shutdown by Apple
- DOJ Calls Out Apple on Beeper
- LoopMessage Legal FAQ
Next Steps When Ready to Implement¶
- Validate B2B demand - Talk to potential customers (bathroom remodelers, airlines)
- Choose CPaaS provider - Twilio vs Vonage vs MessageBird
- Start with Phase 1 - Channel abstraction is non-breaking
- Set up Twilio account - Get messaging service SID and numbers
- Prototype web chat - Zero-cost way to test B2B flow
This document should be updated as the architecture evolves or new requirements emerge.