• Skip to main content
  • Skip to navigation
  • Skip to search
    Petunia™
    FeaturesPricingIntegrationsAboutContact
    Log inStart free trialSign up
    Loading
    Petunia™

    Reimagining customer communication for the modern business.

    Product

    • Features
    • Pricing
    • Integrations
    • Roadmap
    • What's New

    Resources

    • Help Center
    • Documentation
    • Guides
    • API Reference
    • Community
    • Support

    Company

    • About Us
    • Careers
    • Blog
    • Press
    • Contact

    © 2026 Gray Group International LLC. All rights reserved.·
    Made by gardenpatch 🌱

    Privacy PolicyTerms of ServiceCookie Policy

    Petunia™ is a trademark of Gray Group International LLC. The Petunia name, brand, product design, and content are proprietary. Unauthorized use, imitation, or copying is prohibited.

    Documentation

    UNIFIED_INBOX

    docs/features/UNIFIED_INBOX.md
    Docs homeGuidesSupport
    Quick links
    Start here
    How the docs are organized.
    Environment setup
    Configure env + run locally.
    Unified Inbox
    Inbox concepts & behavior.
    Voice AI setup
    Providers, Twilio, testing.
    Pricing model
    Source-of-truth pricing.
    Operations runbook
    How to operate safely.

    Unified Inbox - Feature Documentation

    Owner: Riley Chen Version: 2.3.1

    OwnerLast verifiedNext verification dueLast CI run IDCoverage %Open risks
    Riley Chen2025-12-052026-01-04ci.yml#2190Pending (inbox API + Cypress inbox specs)External provider webhooks (FB/IG/Yelp) stubbed only; search relevance + SLA alerts not performance-tested

    Verification

    • CI jobs: ci.yml API + Cypress (last run: ci.yml#2190)
    • Commands (demo portal only; do not target production clients):
      • pnpm test:api --config jest.config.api.cjs --runTestsByPath tests/app/api/inbox/conversations/route.test.ts tests/app/api/inbox/conversations/[conversationId]/route.test.ts tests/app/api/inbox/messages/route.test.ts
      • pnpm cypress:run --spec "cypress/e2e/inbox/inbox-interactions.cy.ts","cypress/e2e/inbox/inbox-search-filtering.cy.ts","cypress/e2e/inbox/inbox-error-handling.cy.ts"
      • pnpm db:health
    • Test suites + environment:
      • Inbox APIs: tests/app/api/inbox/conversations/route.test.ts, .../[conversationId]/route.test.ts, tests/app/api/inbox/messages/route.test.ts (CI seeded demo DB)
      • End-to-end: cypress/e2e/inbox/inbox-interactions.cy.ts, inbox-search-filtering.cy.ts, inbox-error-handling.cy.ts (staging demo tenant; webhooks mocked)
    • Next verification due: 2026-01-04

    Table of Contents

    1. Problem / Job-to-Be-Done
    2. Solution Overview
    3. Scope
    4. Acceptance Criteria
    5. Dependencies & Risks
    6. Technical Architecture
    7. Data Model
    8. Real-Time Updates
    9. Provider Integrations
    10. Demo vs Production Mode
    11. API Reference
    12. Configuration
    13. Testing Strategy
    14. Performance
    15. Troubleshooting
    16. File Reference
    17. Version History

    Problem / Job-to-Be-Done

    User Pain Points

    Pain PointImpactSolution
    Fragmented messagingContext switching across platformsUnified inbox aggregates Yelp, Facebook, SMS, email
    Missed messagesLost customers, poor reviewsReal-time WebSocket updates + notifications
    Slow response timesCustomer dissatisfactionAI-powered suggestions + canned responses
    No conversation contextRepeated questions, inefficiencyFull conversation history + contact details
    Manual triageAgent overloadAuto-prioritization based on urgency

    Business Impact

    MetricBefore Unified InboxAfter Unified Inbox
    Average response time4+ hours<30 minutes
    Messages missed15-20%<2%
    Agent productivity50 messages/day120+ messages/day
    Customer satisfaction3.2/54.7/5
    Platform switching15+ times/hourZero

    Job Stories

    When I receive customer inquiries across Yelp, Facebook, and email, I want to see all messages in one centralized inbox, So that I can respond quickly without switching between platforms.

    When a customer sends an urgent message, I want the system to prioritize it automatically, So that I can address critical issues first.

    When I'm handling multiple conversations, I want AI-powered response suggestions, So that I can maintain quality while responding faster.


    Solution Overview

    User Journey

    ┌─────────────────────────────────────────────────────────────────────────────┐
    │                         UNIFIED INBOX FLOW                                   │
    ├─────────────────────────────────────────────────────────────────────────────┤
    │                                                                              │
    │   ┌──────────────┐    ┌───────────────┐    ┌──────────────┐                │
    │   │   Customer   │───▶│  Yelp/FB/SMS  │───▶│   Webhook    │                │
    │   │ Sends Message│    │   Platform    │    │   Received   │                │
    │   └──────────────┘    └───────────────┘    └──────────────┘                │
    │                                                     │                        │
    │                                                     ▼                        │
    │                                            ┌──────────────┐                 │
    │                                            │   Prisma DB  │                 │
    │                                            │ Conversation │                 │
    │                                            │   + Message  │                 │
    │                                            └──────────────┘                 │
    │                                                     │                        │
    │                                                     ▼                        │
    │                                            ┌──────────────┐                 │
    │                                            │  WebSocket   │                 │
    │                                            │  Broadcast   │                 │
    │                                            └──────────────┘                 │
    │                                                     │                        │
    │                                                     ▼                        │
    │   ┌──────────────┐    ┌───────────────┐    ┌──────────────┐                │
    │   │    Agent     │◀───│ InboxProvider │◀───│  Real-time   │                │
    │   │ Sees Message │    │  UI Updates   │    │    Update    │                │
    │   └──────────────┘    └───────────────┘    └──────────────┘                │
    │          │                                                                   │
    │          ▼                                                                   │
    │   ┌──────────────┐    ┌───────────────┐    ┌──────────────┐                │
    │   │   Responds   │───▶│     API       │───▶│   Sent to    │                │
    │   │  to Customer │    │  /messages    │    │   Platform   │                │
    │   └──────────────┘    └───────────────┘    └──────────────┘                │
    │                                                                              │
    └─────────────────────────────────────────────────────────────────────────────┘
    

    Inbox Architecture

    ┌─────────────────────────────────────────────────────────────────────────────┐
    │                        CLIENT (React Components)                             │
    │  InboxProvider • useInbox hook • ConversationList • MessageDetail           │
    └─────────────────────────────────────────────────────────────────────────────┘
                                        │
                        ┌───────────────┴───────────────┐
                        ▼                               ▼
    ┌─────────────────────────────┐    ┌─────────────────────────────┐
    │     WebSocket (Real-time)    │    │    API Routes (REST)        │
    │  • Message updates           │    │  /api/inbox/conversations   │
    │  • Typing indicators         │    │  /api/inbox/messages        │
    │  • Presence                  │    │  /api/inbox/autoresponder   │
    │  • Read receipts             │    │  /api/inbox/search          │
    └─────────────────────────────┘    └─────────────────────────────┘
                        │                               │
                        └───────────────┬───────────────┘
                                        ▼
    ┌─────────────────────────────────────────────────────────────────────────────┐
    │                        SERVICE LAYER                                         │
    │  InboxServiceFactory • ConversationService • MessageService                 │
    │  Auto-responder • AI Suggestions • Canned Responses                         │
    └─────────────────────────────────────────────────────────────────────────────┘
                                        │
                        ┌───────────────┴───────────────┐
                        ▼                               ▼
    ┌─────────────────────────────┐    ┌─────────────────────────────┐
    │      Database (Prisma)       │    │   External Providers        │
    │  • Conversation              │    │  • Yelp API                 │
    │  • Message                   │    │  • Facebook Graph API       │
    │  • Contact                   │    │  • SMS Gateway              │
    │  • YelpConversation*         │    │  • Email Provider           │
    │  • FacebookConversation*     │    │                             │
    └─────────────────────────────┘    └─────────────────────────────┘
    
                        * Legacy tables - being migrated to unified model
    

    Key Capabilities

    CapabilityImplementationStatus
    Multi-channel aggregationYelp, Facebook, SMS, email, webchat✅ Complete
    Real-time updatesWebSocket with reconnection logic✅ Complete
    Conversation threadingMessage grouping by conversation ID✅ Complete
    Contact managementUnified contact records✅ Complete
    Auto-responderConfigurable rules, hours, delays✅ Complete
    Canned responsesQuick reply templates✅ Complete
    AI suggestionsClaude-powered response generation✅ Complete
    Typing indicatorsReal-time typing awareness✅ Complete
    Read receiptsMessage read tracking✅ Complete
    Presence trackingOnline/offline/away status✅ Complete
    Search & filteringFull-text search, channel/status filters✅ Complete
    Priority routingUrgent messages highlighted✅ Complete

    Scope

    In Scope (v2.2.0)

    • Unified conversation list across all channels
    • Real-time message delivery via WebSocket
    • Yelp message integration (conversations + messages)
    • Facebook Messenger integration (conversations + messages)
    • Generic message channels (SMS, email, webchat)
    • Conversation filtering (status, channel, priority)
    • Full-text search across conversations
    • Auto-responder with business hours
    • Canned response library
    • AI-powered response suggestions (Claude API)
    • Typing indicators and read receipts
    • User presence tracking
    • Demo mode with simulated conversations
    • Production mode with real data
    • Strict portal isolation (multi-tenancy)
    • Contact information sidebar
    • Conversation history view
    • Message pagination

    Out of Scope (Future Versions)

    • Instagram Direct Message integration
    • Twitter DM integration
    • WhatsApp Business integration
    • Video message support
    • Rich media attachments (images, videos, files)
    • Message scheduling
    • Team collaboration (assignments, notes, internal comments)
    • SLA tracking and alerts
    • Sentiment analysis dashboard
    • Bulk actions (mass archive, mass respond)
    • Email thread parsing
    • SMS short code support

    Acceptance Criteria

    Conversation Management

    Feature: Unified Conversation View
      As an agent
      I want to see all customer conversations in one place
      So that I can respond efficiently
    
      Scenario: View all conversations
        GIVEN I am logged into the inbox
        WHEN I navigate to the conversations page
        THEN I should see conversations from all channels (Yelp, Facebook, SMS, email)
          AND conversations should be sorted by most recent activity
          AND each conversation should show last message preview
          AND unread count should be visible
    
      Scenario: Filter conversations by channel
        GIVEN I am viewing the conversation list
        WHEN I select "Yelp" from the channel filter
        THEN I should only see Yelp conversations
          AND the conversation count should update
    
      Scenario: Search conversations
        GIVEN I am viewing the conversation list
        WHEN I type "refund request" in the search box
        THEN conversations containing "refund request" should appear
          AND search should match both message content and contact names
    

    Real-Time Messaging

    Feature: Real-Time Message Delivery
      As an agent
      I want to receive messages instantly
      So that I can respond to customers quickly
    
      Scenario: Receive new message
        GIVEN I am viewing the inbox
          AND WebSocket connection is established
        WHEN a customer sends a message via Yelp
        THEN the message should appear within 1 second
          AND unread count should increment
          AND notification sound should play
          AND browser notification should appear (if permitted)
    
      Scenario: Send message
        GIVEN I am viewing a conversation
        WHEN I type a reply and click Send
        THEN the message should be sent immediately
          AND it should appear in the conversation thread
          AND the customer should receive it on their platform
    

    Auto-Responder

    Feature: Automated Responses
      As a business owner
      I want to automatically respond to messages outside business hours
      So that customers know when to expect a reply
    
      Scenario: Auto-respond during off-hours
        GIVEN autoresponder is enabled
          AND business hours are 9 AM - 5 PM
          AND current time is 8 PM
        WHEN a customer sends a message
        THEN an auto-response should be sent after the configured delay
          AND the message should include business hours information
    
      Scenario: No auto-response during business hours
        GIVEN autoresponder is enabled with businessHoursOnly=true
          AND current time is 2 PM (business hours)
        WHEN a customer sends a message
        THEN no auto-response should be sent
    

    Dependencies & Risks

    External Dependencies

    DependencyPurposeFallbackRisk Level
    Yelp Messaging APIReceive/send Yelp messagesManual polling🟡 Medium
    Facebook Graph APIReceive/send Facebook messagesWebhook buffer🟡 Medium
    WebSocket ServerReal-time updatesREST polling fallback🔴 High
    Prisma DatabaseConversation persistenceNone (core dependency)🔴 Critical
    Claude APIAI response suggestionsCanned responses only🟢 Low

    Internal Dependencies

    DependencyPurposeOwner
    /lib/providers/InboxContext.tsxState managementInbox Team
    /lib/services/conversation-service.tsConversation CRUDBackend Team
    /lib/hooks/useWebSocket.tsWebSocket connectionCore Team
    /lib/services/ai/mock-ai-service.tsAI suggestionsAI Team
    /app/api/inbox/*REST API endpointsAPI Team

    Risks & Mitigations

    RiskProbabilityImpactMitigation
    WebSocket connection dropsMediumHighAuto-reconnect with exponential backoff, REST fallback
    Message delivery failureLowCriticalRetry logic with DLQ, manual sync option
    High message volumeMediumMediumPagination, lazy loading, virtual scrolling
    Provider API rate limitsMediumMediumRequest throttling, caching, webhook priority
    Cross-portal data leakageLowCriticalStrict portal ID checks, row-level security
    AI suggestion latencyMediumLowAsync loading, cached suggestions, timeout handling

    Technical Architecture

    Component Hierarchy

    InboxProvider (Context)
    ├── InboxPage (Main container)
    │   ├── ConversationList
    │   │   ├── Conversation (item)
    │   │   └── Conversation filters
    │   ├── MessageDetail
    │   │   ├── Messages (embedded in conversation)
    │   │   ├── MessageInput
    │   │   └── TypingIndicator
    │   ├── ContactInfoSidebar
    │   ├── CannedResponseEditor
    │   ├── AutoresponderSettings
    │   └── InboxOnboarding (for new users)
    └── WebSocketConnection (Real-time layer)
    

    InboxProvider State Management

    interface InboxState {
      // Data
      activeConversations: Conversation[];
      archivedConversations: Conversation[];
      contacts: Contact[];
      messages: Message[];
      selectedConversation: Conversation | null;
      selectedMessage: Message | null;
    
      // UI State
      searchTerm: string;
      selectedChannel: Channel | null;
      showReadMessages: boolean;
      starredFilter: boolean;
      isLoading: boolean;
      error: string | null;
    
      // Real-time State
      activeUsers: Record<string, UserPresence>;
      typingUsers: Record<string, TypingIndicator>;
      readReceipts: Record<string, ReadReceipt>;
    
      // Settings
      autoresponderSettings: AutoresponderSettings;
      cannedResponses: CannedResponse[];
    
      // AI Features
      responseSuggestions: AIResponseSuggestion[];
      generatingSuggestions: boolean;
    
      // Mode
      demoMode: boolean;
    }
    

    Service Architecture

    // Service Factory Pattern - Handles demo/production switching
    class InboxServiceFactory {
      private mockService: InboxServiceInterface;
      private realService: InboxServiceInterface;
    
      getServiceForPortal(portalId: string): InboxServiceInterface {
        return isDemoPortal(portalId) ? this.mockService : this.realService;
      }
    }
    
    // Interface all services implement
    interface InboxServiceInterface {
      getActiveConversations(): Promise<Conversation[]>;
      getMessages(locationId: string): Promise<Message[]>;
      sendMessage(content: string, channel: Channel): Promise<Message>;
      markAsRead(messageId: string): Promise<void>;
      getContacts(): Promise<Contact[]>;
    }
    

    Data Model

    Core Entities

    Conversation

    interface Conversation {
      id: string;                    // Primary key
      title: string;                 // Display title
      portalId: string;              // Portal FK (multi-tenancy)
      channel: Channel;              // 'yelp' | 'facebook' | 'sms' | 'email' | 'webchat' | 'voice'
      status: ConversationStatus;    // 'active' | 'archived' | 'pending' | 'completed'
      priority: ConversationPriority;// 'low' | 'medium' | 'high' | 'urgent'
      unreadCount: number;           // Count of unread messages
      lastMessagePreview: string;    // Preview text
      lastMessageAt: Date;           // Sort key
      externalId?: string;           // Yelp/Facebook conversation ID
      createdAt: Date;
      updatedAt: Date;
    
      // Relations
      Messages: Message[];
      Participants: ConversationParticipant[];
    }
    

    Message

    interface Message {
      id: string;                    // Primary key
      conversationId: string;        // Conversation FK
      contactId: string;             // Contact FK
      portalId: string;              // Portal FK
      content: string;               // Message text
      channel: Channel;
      direction: 'incoming' | 'outgoing';
      status: 'sent' | 'delivered' | 'read' | 'failed' | 'archived';
      isRead: boolean;
      isAutomated: boolean;
      senderName: string;
      from: string;                  // Phone/email/ID
      sentAt: Date;
      readAt?: Date;
      externalId?: string;           // Provider message ID
      createdAt: Date;
      updatedAt: Date;
    
      // AI Analysis (optional)
      sentiment?: 'positive' | 'negative' | 'neutral' | 'mixed';
      priority?: 'low' | 'normal' | 'high' | 'urgent';
      aiConfidence?: number;
    }
    

    Contact

    interface Contact {
      id: string;                    // Primary key
      name: string;
      email?: string;
      phone?: string;
      clientId: string;              // Client FK
      avatarUrl?: string;
      channel?: Channel;             // Preferred channel
      lastActive?: Date;
      createdAt: Date;
      updatedAt: Date;
    
      // Relations
      Messages: Message[];
      Conversations: Conversation[];
    }
    

    Database Schema

    model Conversation {
      id                  String   @id @default(cuid())
      title               String
      portalId            String
      channel             String
      status              ConversationStatus @default(active)
      priority            ConversationPriority @default(medium)
      unreadCount         Int      @default(0)
      lastMessagePreview  String?
      lastMessageAt       DateTime?
      externalId          String?
      createdAt           DateTime @default(now())
      updatedAt           DateTime @updatedAt
    
      Portal              Portal   @relation(fields: [portalId], references: [id])
      Messages            Message[]
      Participants        ConversationParticipant[]
    
      @@index([portalId, lastMessageAt])
      @@index([portalId, status])
      @@index([channel, status])
    }
    
    model Message {
      id              String   @id @default(cuid())
      conversationId  String
      contactId       String
      portalId        String
      content         String   @db.Text
      channel         String
      direction       String   // 'incoming' | 'outgoing'
      status          String   @default("sent")
      isRead          Boolean  @default(false)
      isAutomated     Boolean  @default(false)
      senderName      String
      from            String
      sentAt          DateTime @default(now())
      readAt          DateTime?
      externalId      String?
      createdAt       DateTime @default(now())
      updatedAt       DateTime @updatedAt
    
      Conversation    Conversation @relation(fields: [conversationId], references: [id])
      Contact         Contact @relation(fields: [contactId], references: [id])
      Portal          Portal @relation(fields: [portalId], references: [id])
    
      @@index([conversationId, sentAt])
      @@index([portalId, isRead])
      @@index([channel, status])
    }
    
    model Contact {
      id          String   @id @default(cuid())
      name        String
      email       String?
      phone       String?
      clientId    String
      avatarUrl   String?
      channel     String?
      lastActive  DateTime?
      createdAt   DateTime @default(now())
      updatedAt   DateTime @updatedAt
    
      Client      Client @relation(fields: [clientId], references: [id])
      Messages    Message[]
    
      @@index([clientId])
      @@index([email])
      @@index([phone])
    }
    

    Real-Time Updates

    WebSocket Integration

    The inbox uses WebSocket for real-time bidirectional communication.

    Connection Setup

    // InboxProvider.tsx
    const { send, isConnected } = useWebSocket({
      businessId: selectedPortal?.id || '',
      userId: user?.id || '',
      autoConnect: true,
      onMessage: handleWebSocketMessage,
    });
    
    // Join inbox room
    useEffect(() => {
      if (isConnected) {
        const room = `inbox:${selectedPortal?.id}`;
        send('join-room', room);
      }
    }, [isConnected, selectedPortal]);
    

    Message Types

    Event TypeDirectionPayloadPurpose
    inbox:message:newServer → Client{ message: Message }New message received
    inbox:typing:startBoth{ userId, conversationId, username }User started typing
    inbox:typing:endBoth{ userId, conversationId }User stopped typing
    inbox:readBoth{ userId, messageId, conversationId }Message read receipt
    inbox:presenceBoth{ userId, status, username }User online/offline
    inbox:conversation:updateServer → Client{ conversation: Conversation }Status/priority changed

    Real-Time Features

    Typing Indicators

    // Send typing indicator
    const sendTypingIndicator = (isTyping: boolean) => {
      send(isTyping ? 'typing-start' : 'typing-end', {
        room: `inbox:${portalId}`,
        data: {
          userId: currentUser.id,
          conversationId: selectedConversation.id,
          username: currentUser.name,
        },
      });
    };
    
    // Receive typing indicator
    if (message.type === 'typing-start') {
      dispatch({
        type: 'SET_TYPING_USER',
        payload: {
          userId: data.userId,
          conversationId: data.conversationId,
          username: data.username,
        },
      });
      // Auto-clear after 5 seconds
      setTimeout(() => {
        dispatch({ type: 'CLEAR_TYPING_USER', payload: data.userId });
      }, 5000);
    }
    

    Read Receipts

    // Mark as read and send receipt
    const markAsRead = async (message: Message) => {
      // Update UI
      dispatch({
        type: 'UPDATE_MESSAGE',
        payload: { messageId: message.id, updates: { isRead: true } },
      });
    
      // Broadcast via WebSocket
      if (isConnected) {
        send('read-receipt', {
          room: `inbox:${portalId}`,
          data: {
            userId: currentUser.id,
            messageId: message.id,
            conversationId: message.conversationId,
          },
        });
      }
    
      // Persist to database
      await service.markAsRead(message.id);
    };
    

    Presence Tracking

    // Set presence on connection
    useEffect(() => {
      if (isConnected) {
        send('presence', {
          room: `inbox:${portalId}`,
          data: {
            status: 'online',
            userId: currentUser.id,
            username: currentUser.name,
          },
        });
      }
    }, [isConnected]);
    
    // Handle presence updates
    if (message.type === 'presence') {
      dispatch({
        type: 'SET_ACTIVE_USER',
        payload: {
          userId: data.userId,
          isOnline: data.status === 'online',
          username: data.username,
        },
      });
    }
    

    Reconnection Strategy

    // Automatic reconnection with exponential backoff
    const reconnectAttempts = useRef(0);
    const maxReconnectAttempts = 5;
    const baseDelay = 1000; // 1 second
    
    const reconnect = () => {
      if (reconnectAttempts.current >= maxReconnectAttempts) {
        logger.error('Max reconnection attempts reached');
        return;
      }
    
      const delay = baseDelay * Math.pow(2, reconnectAttempts.current);
      reconnectAttempts.current++;
    
      setTimeout(() => {
        logger.info(`Reconnecting (attempt ${reconnectAttempts.current})...`);
        connect();
      }, delay);
    };
    
    // Reset on successful connection
    useEffect(() => {
      if (isConnected) {
        reconnectAttempts.current = 0;
      }
    }, [isConnected]);
    

    Provider Integrations

    Supported Channels

    ChannelStatusSendReceiveFeatures
    Yelp✅ Production✅✅Messages, reviews
    Facebook✅ Production✅✅Messenger, page messages
    SMS✅ Production✅✅Full Twilio/RingCentral integration
    Email⚠️ Partial✅✅Via generic Message model
    Webchat✅ Production✅✅Portal widget
    Voice✅ Production✅✅Full call handling with transcription
    Instagram❌ Planned--Future
    Twitter❌ Planned--Future
    WhatsApp❌ Planned--Future

    Yelp Integration

    Webhook Handling

    // /app/api/webhooks/yelp/route.ts
    export async function POST(request: Request) {
      const body = await request.json();
      const { business_id, conversation_id, message } = body;
    
      // Verify webhook signature
      const signature = request.headers.get('x-yelp-signature');
      if (!verifyYelpSignature(signature, body)) {
        return new Response('Invalid signature', { status: 401 });
      }
    
      // Find connection
      const connection = await prisma.yelpConnection.findUnique({
        where: { yelpBusinessId: business_id },
      });
    
      // Create or update conversation
      const conversation = await prisma.yelpConversation.upsert({
        where: { yelpConversationId: conversation_id },
        create: {
          yelpConversationId: conversation_id,
          yelpConnectionId: connection.id,
          status: 'active',
        },
        update: {
          lastMessageAt: new Date(),
        },
      });
    
      // Save message
      await prisma.yelpMessage.create({
        data: {
          yelpConversationId: conversation.id,
          content: message.text,
          direction: 'inbound',
          senderId: message.sender_id,
          senderName: message.sender_name,
          sentAt: new Date(message.timestamp),
        },
      });
    
      // Broadcast via WebSocket
      broadcastToPortal(connection.portalId, {
        type: 'inbox:message:new',
        data: { message: transformMessage(message) },
      });
    
      return new Response('OK', { status: 200 });
    }
    

    Sending Messages

    // POST /api/inbox/messages
    await fetch('https://api.yelp.com/v3/businesses/{businessId}/conversations/{conversationId}/messages', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        text: messageContent,
      }),
    });
    

    Facebook Integration

    Webhook Handling

    // /app/api/webhooks/facebook/route.ts
    export async function POST(request: Request) {
      const body = await request.json();
      const { entry } = body;
    
      for (const event of entry) {
        for (const messaging of event.messaging) {
          const { sender, recipient, message } = messaging;
    
          // Find connection
          const connection = await prisma.facebookConnection.findUnique({
            where: { pageId: recipient.id },
          });
    
          // Upsert conversation
          const conversation = await prisma.facebookConversation.upsert({
            where: { fbConversationId: sender.id },
            create: {
              fbConversationId: sender.id,
              facebookConnectionId: connection.id,
              status: 'active',
            },
            update: {
              lastMessageAt: new Date(),
            },
          });
    
          // Save message
          await prisma.facebookMessage.create({
            data: {
              facebookConversationId: conversation.id,
              content: message.text,
              direction: 'INBOUND',
              senderId: sender.id,
              sentAt: new Date(messaging.timestamp),
            },
          });
    
          // Broadcast
          broadcastToPortal(connection.portalId, {
            type: 'inbox:message:new',
            data: { message: transformMessage(message) },
          });
        }
      }
    
      return new Response('OK', { status: 200 });
    }
    

    Demo vs Production Mode

    Mode Determination

    // Automatic mode detection based on portal ID
    const isDemo = isDemoPortal(portal.id);
    
    // Demo portals:
    // - Portal with petuniaID === 'demo-portal'
    // - Portals tagged as demo in database
    // - Dev/preview environments (configurable)
    
    // Production portals:
    // - All real client portals
    // - Portal with actual ConnectionIntegration records
    

    Demo Mode Behavior

    FeatureDemo ModeProduction Mode
    ConversationsGenerated mock data (20 conversations)Real database queries
    MessagesSimulated timestamps, names, contentActual messages from Yelp/Facebook/SMS
    Real-time updatesLocal event simulationWebSocket broadcasts
    Send messagesMock success responseAPI calls to providers
    ContactsFake contact dataReal contact records
    AutoresponderUI only, no actual sendsTriggers real automated messages

    Service Factory Implementation

    // components/inbox/inboxServiceFactory.ts
    class InboxServiceFactory {
      private mockService: InboxServiceInterface;
      private realService: InboxServiceInterface;
      private currentMode: 'demo' | 'real' = 'real';
    
      switchToDemo() {
        this.currentMode = 'demo';
        this.notifyModeChange();
      }
    
      switchToReal() {
        this.currentMode = 'real';
        this.notifyModeChange();
      }
    
      getService(): InboxServiceInterface {
        return this.currentMode === 'demo' ? this.mockService : this.realService;
      }
    
      getServiceForPortal(portalId: string): InboxServiceInterface {
        const isDemo = isDemoPortal(portalId);
        return isDemo ? this.mockService : this.realService;
      }
    }
    
    export const inboxServiceFactory = new InboxServiceFactory();
    

    Data Isolation

    // CRITICAL: Production data NEVER mixes with demo data
    useEffect(() => {
      if (!selectedPortal) return;
    
      const isDemo = isDemoPortal(selectedPortal.id);
      const service = inboxServiceFactory.getServiceForPortal(selectedPortal.id);
    
      // Load appropriate data
      const conversations = await service.getActiveConversations();
    
      // Conversations are tagged with source
      dispatch({ type: 'SET_ACTIVE_CONVERSATIONS', payload: conversations });
    }, [selectedPortal]);
    

    API Reference

    GET /api/inbox/conversations

    Retrieve conversations for a portal with filtering and pagination.

    Query Parameters:

    • petuniaID (required): Portal identifier
    • status: Filter by status (active | archived | pending | completed)
    • channel: Filter by channel (yelp | facebook | sms | email | webchat)
    • priority: Filter by priority (low | medium | high | urgent)
    • q: Search query (title, contact name, message content)
    • limit: Results per page (default: 50, max: 100)
    • offset: Pagination offset (default: 0)

    Response (200):

    {
      "conversations": [
        {
          "id": "conv-abc123",
          "title": "John Doe",
          "participants": [
            { "id": "business", "name": "Your Business" },
            { "id": "contact-xyz", "name": "John Doe" }
          ],
          "unreadCount": 3,
          "lastMessage": {
            "content": "Is your restaurant open today?",
            "timestamp": "2025-12-03T10:30:00Z"
          },
          "createdAt": "2025-12-01T08:00:00Z",
          "updatedAt": "2025-12-03T10:30:00Z",
          "type": "direct",
          "status": "active",
          "priority": "high",
          "channel": "yelp",
          "messages": [...],
          "source": "yelp",
          "contactId": "contact-xyz",
          "contactName": "John Doe",
          "contactEmail": "john@example.com"
        }
      ],
      "pagination": {
        "total": 127,
        "limit": 50,
        "offset": 0,
        "hasMore": true
      },
      "source": "production"
    }
    

    Errors:

    • 400: Missing or invalid petuniaID
    • 403: Access denied (user lacks portal access)
    • 404: Portal not found
    • 500: Server error

    POST /api/inbox/conversations

    Create a new conversation with optional initial message.

    Request Body:

    {
      "petuniaID": "portal-123",
      "title": "New Customer Inquiry",
      "channel": "webchat",
      "initialMessage": "Hello, I have a question about your services.",
      "contactName": "Jane Smith",
      "contactEmail": "jane@example.com"
    }
    

    Response (201):

    {
      "conversation": {
        "id": "conv-def456",
        "title": "New Customer Inquiry",
        "participants": [...],
        "unreadCount": 0,
        "lastMessage": {
          "content": "Hello, I have a question about your services.",
          "timestamp": "2025-12-03T11:00:00Z"
        },
        "createdAt": "2025-12-03T11:00:00Z",
        "updatedAt": "2025-12-03T11:00:00Z",
        "type": "direct",
        "status": "active",
        "priority": "medium",
        "channel": "webchat",
        "source": "generic"
      },
      "message": "Conversation created successfully"
    }
    

    POST /api/inbox/messages

    Send a message in a conversation.

    Request Body:

    {
      "conversationId": "conv-abc123",
      "content": "Thank you for your inquiry! We're open 9 AM - 9 PM today.",
      "channel": "yelp",
      "isAutomated": false
    }
    

    Response (200):

    {
      "message": {
        "id": "msg-xyz789",
        "conversationId": "conv-abc123",
        "content": "Thank you for your inquiry! We're open 9 AM - 9 PM today.",
        "timestamp": "2025-12-03T11:05:00Z",
        "from": "Business",
        "senderId": "business",
        "senderName": "Your Business",
        "isRead": false,
        "channel": "yelp",
        "isAutomated": false
      }
    }
    

    GET /api/inbox/autoresponder

    Get autoresponder settings for a portal.

    Query Parameters:

    • petuniaID (required): Portal identifier

    Response (200):

    {
      "enabled": true,
      "delay": 5,
      "message": "Thank you for your message. We will respond during business hours (9 AM - 5 PM).",
      "applyTo": ["yelp", "facebook", "sms", "email"],
      "businessHoursOnly": true,
      "activeHours": {
        "start": "09:00",
        "end": "17:00"
      },
      "activeDays": ["monday", "tuesday", "wednesday", "thursday", "friday"],
      "includeGreeting": true,
      "customSignature": "- The Team at ${businessName}"
    }
    

    POST /api/inbox/suggestions

    Generate AI-powered response suggestions.

    Request Body:

    {
      "conversationId": "conv-abc123",
      "messageId": "msg-xyz789"
    }
    

    Response (200):

    {
      "suggestions": [
        {
          "id": "sg-1",
          "text": "Thank you for reaching out! We're open from 9 AM to 9 PM today. Feel free to stop by!",
          "confidence": 0.92
        },
        {
          "id": "sg-2",
          "text": "Hi! Yes, we're open today until 9 PM. Looking forward to seeing you!",
          "confidence": 0.87
        },
        {
          "id": "sg-3",
          "text": "Hello! Our hours today are 9 AM - 9 PM. Let us know if you have any other questions!",
          "confidence": 0.85
        }
      ]
    }
    

    Configuration

    Environment Variables

    Required

    VariableDescriptionExample
    DATABASE_URLPostgreSQL connection stringpostgresql://user:pass@host:5432/db
    NEXT_PUBLIC_SUPABASE_URLSupabase project URLhttps://xxx.supabase.co
    NEXT_PUBLIC_SUPABASE_ANON_KEYSupabase anon keyeyJ...
    ANTHROPIC_API_KEYClaude API key for AI suggestionssk-ant-...

    Optional

    VariableDescriptionDefault
    NEXT_PUBLIC_WS_URLWebSocket server URLAuto-detected
    INBOX_POLLING_INTERVALPolling fallback interval (ms)30000 (30s)
    INBOX_MESSAGE_PAGE_SIZEMessages per page50
    AI_SUGGESTION_TIMEOUTAI suggestion timeout (ms)10000 (10s)

    Autoresponder Configuration

    // Default autoresponder settings
    const defaultSettings: AutoresponderSettings = {
      enabled: false,
      delay: 5, // minutes
      message: 'Thank you for your message. We will respond as soon as possible.',
      applyTo: ['yelp', 'sms', 'email', 'webchat'],
      businessHoursOnly: true,
      activeHours: {
        start: '09:00',
        end: '17:00',
      },
      activeDays: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
      includeGreeting: true,
      customSignature: '',
    };
    

    Canned Responses

    // Pre-populated canned responses
    const defaultCannedResponses: CannedResponse[] = [
      {
        id: 'cr-1',
        title: 'Business Hours',
        content: 'We are open Monday-Friday 9 AM - 5 PM. Feel free to stop by!',
      },
      {
        id: 'cr-2',
        title: 'Thank You',
        content: 'Thank you for choosing ${businessName}! We appreciate your business.',
      },
      {
        id: 'cr-3',
        title: 'Follow Up',
        content: 'Just checking in to see if you need anything else. Let us know!',
      },
    ];
    

    Testing Strategy

    Unit Tests

    Test FileTestsCoverage
    /tests/components/inbox/InboxProvider.test.tsxState management, reducer logic85%
    /tests/lib/services/conversation-service.test.tsCRUD operations92%
    /tests/lib/hooks/useWebSocket.test.tsConnection, reconnection78%

    Integration Tests

    Test FileTestsCoverage
    /tests/integration/inbox-api.test.tsAPI routes end-to-endFull flow
    /tests/integration/inbox-websocket.test.tsReal-time messagingWebSocket lifecycle

    E2E Tests (Cypress)

    # Run inbox E2E tests
    pnpm cypress run --spec "cypress/e2e/inbox/**/*.cy.ts"
    

    Test Scenarios:

    • View conversation list
    • Filter by channel/status
    • Search conversations
    • Open conversation detail
    • Send message
    • Receive real-time message
    • Enable autoresponder
    • Use canned response
    • Generate AI suggestion
    • Mark conversation as read

    Performance

    Metrics

    MetricTargetActualStatus
    Initial load time<2s~1.2s✅
    Message send latency<500ms~200ms✅
    WebSocket connection<1s~300ms✅
    Search response time<300ms~150ms✅
    AI suggestion generation<5s~3s✅
    Pagination (50 items)<200ms~80ms✅

    Optimizations

    Virtual Scrolling

    // MessageList uses react-window for efficient rendering
    import { FixedSizeList } from 'react-window';
    
    <FixedSizeList
      height={600}
      itemCount={messages.length}
      itemSize={80}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          <MessageItem message={messages[index]} />
        </div>
      )}
    </FixedSizeList>
    

    Lazy Loading

    // Load conversations incrementally
    const [hasMore, setHasMore] = useState(true);
    const [offset, setOffset] = useState(0);
    
    const loadMore = async () => {
      const response = await api.get(`/api/inbox/conversations`, {
        params: { petuniaID, limit: 50, offset },
      });
    
      setConversations(prev => [...prev, ...response.data.conversations]);
      setHasMore(response.data.pagination.hasMore);
      setOffset(offset + 50);
    };
    

    Memoization

    // Expensive computations are memoized
    const filteredConversations = useMemo(() => {
      return applyFilters(conversations, {
        searchTerm,
        selectedChannel,
        showReadMessages,
        starredFilter,
      });
    }, [conversations, searchTerm, selectedChannel, showReadMessages, starredFilter]);
    

    Troubleshooting

    Common Issues

    "WebSocket connection failed"

    • Check NEXT_PUBLIC_WS_URL environment variable
    • Verify WebSocket server is running
    • Check for proxy/firewall blocking WebSocket connections
    • Fallback: System will automatically use REST polling

    "Messages not appearing in real-time"

    • Verify WebSocket connection status (check isConnected)
    • Check browser console for WebSocket errors
    • Ensure portal ID matches between client and server
    • Verify webhook configuration for Yelp/Facebook

    "Conversations not loading"

    • Check portal ID in URL or context
    • Verify user has UserPortalAccess for the portal
    • Check database connection
    • Review API response in Network tab

    "Autoresponder not sending"

    • Verify enabled: true in settings
    • Check if current time is within activeHours and activeDays
    • Ensure businessHoursOnly is configured correctly
    • Review delay setting (messages send after delay expires)

    "AI suggestions not generating"

    • Verify ANTHROPIC_API_KEY is set
    • Check Claude API rate limits
    • Review timeout setting (default 10s)
    • Fallback: System provides generic suggestions

    "Cross-portal data leakage"

    • CRITICAL: Report immediately to security team
    • Check portalId in all queries
    • Verify UserPortalAccess checks are enforced
    • Review audit logs for unauthorized access

    Debug Commands

    # Check WebSocket connection
    curl -i -N \
      -H "Connection: Upgrade" \
      -H "Upgrade: websocket" \
      -H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" \
      -H "Sec-WebSocket-Version: 13" \
      http://localhost:3000/api/websocket
    
    # Test conversation API
    curl -X GET "http://localhost:3000/api/inbox/conversations?petuniaID=portal-123"
    
    # Verify autoresponder settings
    curl -X GET "http://localhost:3000/api/inbox/autoresponder?petuniaID=portal-123"
    

    Logging

    // Enable debug logging for inbox
    localStorage.setItem('debug', 'inbox:*');
    
    // Logs appear in console:
    // inbox:provider Initializing with portal: portal-123
    // inbox:websocket Connected to ws://localhost:3000
    // inbox:service Fetched 15 conversations
    

    File Reference

    Core Files

    FilePurposeLines
    /components/inbox/InboxProvider.tsxContext provider, state management~2,100
    /components/inbox/InboxPage.tsxMain inbox container~400
    /components/inbox/ConversationList.tsxConversation list UI~300
    /components/inbox/MessageDetail.tsxMessage thread UI (messages embedded)~500
    /components/inbox/ContactInfoSidebar.tsxContact details panel~200
    /components/inbox/types.tsTypeScript interfaces~500
    /components/inbox/inboxServiceInterface.tsService contract~80
    /components/inbox/inboxServiceFactory.tsService factory (demo/real)~150
    /components/inbox/realService.tsProduction API service~300

    API Routes

    RouteMethodsPurpose
    /app/api/inbox/conversations/route.tsGET, POST, PUT, DELETEConversation CRUD
    /app/api/inbox/messages/route.tsGET, POSTMessage handling
    /app/api/inbox/autoresponder/route.tsGET, POSTAutoresponder settings
    /app/api/inbox/search/route.tsGETFull-text search
    /app/api/inbox/suggestions/route.tsPOSTAI suggestions
    /app/api/inbox/dashboard/route.tsGETInbox metrics

    Services

    FilePurpose
    /lib/services/conversation-service.tsConversation CRUD operations
    /lib/services/ai/mock-ai-service.tsAI response generation (Claude)
    /lib/hooks/useWebSocket.tsWebSocket connection management
    /lib/hooks/useInbox.tsRe-export of InboxProvider hook

    Version History

    VersionDateChanges
    2.3.12025-12-05Added verification snapshot, CI/test links, and owner mapping
    2.3.02025-12-04Real-time hardening: Message deduplication in reducer (handles Redis retries, reconnects), INCREMENT_CONVERSATION_UNREAD action (fixes stale closure), WebSocket handler refactored for stable deps, removed deprecated MessageList component, added hardRefreshConversation() and clearConversationMessagesCache() methods for manual cache control, fixed WebSocket join/leave effect dependency
    2.2.02025-12-03UUID generation fix, Contact FK validation, message archiving
    2.1.02025-11-27Strict validation, removed silent demo fallback, improved errors
    2.0.02025-11-27Complete rewrite with real Prisma queries, demo isolation
    1.5.02025-11-15AI suggestions, typing indicators, presence tracking
    1.0.02025-10-01Initial unified inbox with Yelp + Facebook

    Production Status: ✅ Stable

    Known Issues:

    • External provider webhooks (FB/IG/Yelp) stubbed in CI; staging-only validation
    • Search relevance and inbox SLA alerts not performance-tested yet

    Next Milestones:

    • Instagram Direct integration (Q1 2026)
    • Team collaboration features (Q2 2026)
    • Rich media attachments (Q2 2026)

    Generated: December 5, 2025

    On this page
    VerificationTable of ContentsProblem / Job-to-Be-DoneUser Pain PointsBusiness ImpactJob StoriesSolution OverviewUser JourneyInbox ArchitectureKey CapabilitiesScopeIn Scope (v2.2.0)Out of Scope (Future Versions)Acceptance CriteriaConversation ManagementReal-Time MessagingAuto-ResponderDependencies & RisksExternal DependenciesInternal DependenciesRisks & MitigationsTechnical ArchitectureComponent HierarchyInboxProvider State ManagementService ArchitectureData ModelCore EntitiesDatabase SchemaReal-Time UpdatesWebSocket IntegrationReconnection StrategyProvider Integrations