Skip to main content

Real-time Chat Application Architecture and Implementation

Comprehensive low-level design for building a scalable real-time chat system with WebSocket connections, message persistence, and user presence tracking.

This document presents a detailed low-level design for implementing a real-time chat application, covering core architecture patterns, component structure, and technical trade-offs for production-grade frontend systems.

1. Problem Statement & Requirements

Clarifying questions

  1. What target devices/browsers should we support? (e.g., mobile-first, desktop-only, IE11 support)
  2. Do we need real-time messaging capabilities or can we rely on polling/long-polling?
  3. Should the chat support group conversations or be limited to 1:1 messaging?
  4. Do we need offline message persistence and sync when connectivity is restored?
  5. What accessibility requirements exist? (e.g., WCAG AA compliance, screen reader support)
  6. Is i18n support required for multi-language chat content?
  7. Are there existing design system components we should leverage?

Functional requirements

  • Users can view and send text messages in real-time
  • Message history is persisted and displayed when user reconnects
  • Users can see message status (sent, delivered, read)
  • Support for typing indicators and online presence
  • Message search functionality within conversation history
  • Ability to delete or edit own messages (within 15-minute window)
  • User profile information display (avatar, status, last seen)
  • Group chat support with member management
  • Message reactions (emoji support)
  • File attachment capability (images, documents)

Non-functional requirements

  • Performance: Messages should load within 200ms, typing indicators update within 100ms
  • Accessibility: Full keyboard navigation, ARIA roles, screen reader compatibility
  • Scalability: Support for 10k concurrent users with message throughput > 1000 msg/sec
  • Reliability: Message delivery guarantee with retry logic for failed messages
  • Security: End-to-end encryption support (optional)

Constraints and assumptions

  • Backend services are available via REST/GraphQL APIs
  • Authentication handled by external identity provider (OAuth2)
  • Message persistence handled by backend services
  • UI assumes modern browser support (ES6+ features)
  • No third-party chat widget integration required
  • Mobile responsiveness is a hard requirement

Out of scope

  • Voice/video call capabilities
  • Message encryption/decryption (end-to-end)
  • Chat bot integration or automation
  • Push notification handling
  • Message archiving to external storage systems
  • Chat analytics dashboard or reporting features

2. Component Architecture

graph TD
    A[ChatApp] --> B[ChatList]
    A --> C[ChatWindow]
    A --> D[ChatHeader]
    A --> E[MessageInput]
    B --> F[ChatItem]
    C --> G[MessageList]
    C --> H[Message]
    G --> I[MessageBubble]
    E --> J[AttachmentButton]
    E --> K[SendMessageButton]
    
    style A fill:#f0f8ff,stroke:#333
    style B fill:#e6f3ff,stroke:#333
    style C fill:#e6f3ff,stroke:#333
    style D fill:#e6f3ff,stroke:#333
    style E fill:#e6f3ff,stroke:#333
    style F fill:#d0e8ff,stroke:#333
    style G fill:#d0e8ff,stroke:#333
    style H fill:#d0e8ff,stroke:#333
    style I fill:#b3d9ff,stroke:#333
    style J fill:#d0e8ff,stroke:#333
    style K fill:#d0e8ff,stroke:#333

Component Tree Breakdown

Smart Components (Container Components):

  • ChatApp - Main application container managing global state and routing
  • ChatList - Fetches and manages chat list state, handles selection
  • ChatWindow - Manages conversation state, message sending/receiving

Dumb Components (Presentational Components):

  • ChatItem - Displays individual chat preview
  • MessageList - Renders list of messages with proper scrolling
  • MessageBubble - Individual message display with sender info and timestamp
  • MessageInput - Input field with attachment and send capabilities

TypeScript Props Contracts

// ChatApp.tsx
interface ChatAppProps {
  currentUser: User;
  initialChatId?: string;
}

// ChatList.tsx
interface ChatListProps {
  chats: Chat[];
  selectedChatId: string | null;
  onSelectChat: (chatId: string) => void;
  isLoading: boolean;
  error: string | null;
}

// ChatWindow.tsx
interface ChatWindowProps {
  chatId: string;
  currentUser: User;
  onBack?: () => void;
}

// MessageInput.tsx
interface MessageInputProps {
  onSendMessage: (content: string, attachments?: File[]) => void;
  isSending: boolean;
  disabled: boolean;
}

// MessageBubble.tsx
interface MessageBubbleProps {
  message: Message;
  isCurrentUser: boolean;
  showSender?: boolean;
}

UI State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> Composing : onInput
    Composing --> Sending : onSubmit
    Sending --> Success : onSuccess
    Sending --> Error : onError
    Success --> Idle : onReset
    Error --> Composing : onRetry
    Error --> Idle : onDismiss
    
    state Sending {
        [*] --> Pending
        Pending --> Success : success
        Pending --> Error : error
    }

Component Responsibilities

ChatApp

  • Main application shell with routing and authentication context
  • Global error handling and loading states
  • User session management

ChatList

  • Fetches chat list from API
  • Manages selected chat state
  • Displays unread message counts
  • Handles loading and error states

ChatWindow

  • Renders conversation view
  • Manages message history state
  • Handles real-time message updates
  • Integrates with messaging API

MessageInput

  • Captures user input and attachments
  • Validates content before sending
  • Handles send button click and enter key
  • Manages attachment state

Reusability Decisions

  1. MessageBubble is highly reusable across chat interfaces and can be extracted to a shared components library
  2. MessageInput with attachment support is reusable for any messaging context
  3. ChatItem can be reused in notification lists or search results
  4. ChatHeader is specific to chat context but can be extended for group chat features

Component Lifecycle Considerations

  1. Mounting: ChatApp initializes auth, sets up WebSocket connections
  2. Updating: ChatList fetches new chats when user is active, ChatWindow updates message list
  3. Unmounting: Cleanup of WebSocket listeners and timers to prevent memory leaks
  4. Error Boundaries: ChatWindow wraps inner components to handle rendering errors gracefully

The component architecture follows a clear separation of concerns with proper state management boundaries. The UI state machine handles the complex message sending flow with appropriate error handling and user feedback states.

3. State Management Design

Local state vs. global state decisions

For a chat application, we need to carefully distinguish between local and global state:

  • Local state: Component-specific UI state (e.g., message input text, active chat tab, scroll position)
  • Global state: Shared across the application (e.g., user profile, active conversations, message history)
  • Server state: Data fetched from backend (messages, user presence, read receipts)

State management approach

I'll use Zustand as the primary state management solution because:

  1. Lightweight: No boilerplate, minimal bundle size impact
  2. Simple API: Easy to understand and maintain for team onboarding
  3. TypeScript native: Excellent type inference support
  4. Flexible: Can easily integrate with React Query for server state

For server state, I'll combine Zustand with React Query to handle caching, invalidation, and background updates.

State shape and normalization strategy

// Normalized state structure for optimal performance
interface User {
  id: string;
  name: string;
  avatar?: string;
  status: 'online' | 'offline' | 'away';
}

interface Message {
  id: string;
  content: string;
  senderId: string;
  timestamp: Date;
  status: 'sent' | 'delivered' | 'read';
  readBy?: string[]; // User IDs who have read this message
}

interface Conversation {
  id: string;
  participants: User[];
  messages: Message[];
  unreadCount: number;
  lastMessage?: Message;
  isGroup: boolean;
  name?: string; // For group chats
}

interface ChatState {
  currentUser: User | null;
  conversations: Record<string, Conversation>;
  activeConversationId: string | null;
  unreadCounts: Record<string, number>;
  isOnline: boolean;
  typingUsers: Record<string, string[]>; // [conversationId]: [userIds]
}

Server state vs. client state separation

Client state (Zustand):

  • Current user profile
  • Active conversation ID
  • Online status
  • Typing indicators
  • Local message composition

Server state (React Query):

  • Conversation list with pagination
  • Message history for conversations
  • User presence information
  • Read receipts and delivery status

Cache invalidation strategy

  1. Conversation list: Invalidate when new messages arrive or user joins/leaves group
  2. Message history: Invalidate on message send/receive, read receipt updates
  3. User presence: Cache with short TTL (5-10 seconds) for real-time updates
  4. Read receipts: Invalidate when new read receipt is received

Optimistic updates design

For message sending, we implement optimistic updates:

  1. Immediately add message to UI with "sent" status
  2. Update local conversation state
  3. Send to server
  4. If successful, update status to "delivered"
  5. If failed, revert to local state and show error

Critical-path hook implementation

// src/hooks/useChatState.ts
import { create } from 'zustand';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Message, Conversation, User } from '../types';

interface ChatStore {
  currentUser: User | null;
  activeConversationId: string | null;
  conversations: Record<string, Conversation>;
  isOnline: boolean;
  typingUsers: Record<string, string[]>;
  
  // Actions
  setCurrentUser: (user: User | null) => void;
  setActiveConversation: (id: string | null) => void;
  addMessage: (conversationId: string, message: Message) => void;
  updateMessageStatus: (conversationId: string, messageId: string, status: Message['status']) => void;
  setTypingUsers: (conversationId: string, userIds: string[]) => void;
  markConversationAsRead: (conversationId: string) => void;
}

const useChatStore = create<ChatStore>((set, get) => ({
  currentUser: null,
  activeConversationId: null,
  conversations: {},
  isOnline: true,
  typingUsers: {},
  
  setCurrentUser: (user) => set({ currentUser: user }),
  setActiveConversation: (id) => set({ activeConversationId: id }),
  
  addMessage: (conversationId, message) => {
    set((state) => {
      const conversation = state.conversations[conversationId];
      if (!conversation) return state;
      
      // Optimistically update
      const updatedConversation = {
        ...conversation,
        messages: [...conversation.messages, message],
        lastMessage: message,
        unreadCount: conversation.isGroup ? 
          Math.max(0, conversation.unreadCount - 1) : 
          0
      };
      
      return {
        conversations: {
          ...state.conversations,
          [conversationId]: updatedConversation
        }
      };
    });
  },
  
  updateMessageStatus: (conversationId, messageId, status) => {
    set((state) => {
      const conversation = state.conversations[conversationId];
      if (!conversation) return state;
      
      const updatedMessages = conversation.messages.map(msg => 
        msg.id === messageId ? { ...msg, status } : msg
      );
      
      return {
        conversations: {
          ...state.conversations,
          [conversationId]: {
            ...conversation,
            messages: updatedMessages
          }
        }
      };
    });
  },
  
  setTypingUsers: (conversationId, userIds) => {
    set((state) => ({
      typingUsers: {
        ...state.typingUsers,
        [conversationId]: userIds
      }
    }));
  },
  
  markConversationAsRead: (conversationId) => {
    set((state) => {
      const conversation = state.conversations[conversationId];
      if (!conversation) return state;
      
      return {
        conversations: {
          ...state.conversations,
          [conversationId]: {
            ...conversation,
            unreadCount: 0
          }
        }
      };
    });
  }
}));

// Custom hook for fetching and managing conversations
export const useConversations = () => {
  const queryClient = useQueryClient();
  const { data: conversations, isLoading, error } = useQuery({
    queryKey: ['conversations'],
    queryFn: fetchConversations,
    staleTime: 5 * 60 * 1000, // 5 minutes
    cacheTime: 10 * 60 * 1000, // 10 minutes
  });

  const sendMessage = async (conversationId: string, content: string) => {
    const newMessage: Message = {
      id: `msg_${Date.now()}`,
      content,
      senderId: useChatStore.getState().currentUser?.id || '',
      timestamp: new Date(),
      status: 'sent'
    };

    // Optimistic update
    useChatStore.getState().addMessage(conversationId, newMessage);
    
    try {
      const response = await api.sendMessage(conversationId, content);
      
      // Update status to delivered
      useChatStore.getState().updateMessageStatus(
        conversationId, 
        newMessage.id, 
        'delivered'
      );
      
      return response;
    } catch (error) {
      // Revert optimistic update on failure
      useChatStore.getState().updateMessageStatus(
        conversationId, 
        newMessage.id, 
        'sent'
      );
      
      throw error;
    }
  };

  return {
    conversations,
    isLoading,
    error,
    sendMessage
  };
};

// API client for chat operations (simplified)
const api = {
  async sendMessage(conversationId: string, content: string) {
    const response = await fetch(`/api/conversations/${conversationId}/messages`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content })
    });
    
    if (!response.ok) {
      throw new Error('Failed to send message');
    }
    
    return response.json();
  }
};

// Fetch function for React Query
async function fetchConversations() {
  const response = await fetch('/api/conversations');
  if (!response.ok) {
    throw new Error('Failed to fetch conversations');
  }
  return response.json();
}

The hook returns:

  • conversations: List of conversation objects with normalized messages
  • isLoading: Loading state for initial fetch
  • error: Error object if fetch fails
  • sendMessage: Optimistic update function with error handling

Key state transitions:

  1. Message sent → status: 'sent' → 'delivered'
  2. Conversation read → unreadCount: 0
  3. Typing indicator → typingUsers update

Side-effect handler:

  • Message sending with optimistic updates and error rollback
  • Cache invalidation through React Query integration

4. Routing & Navigation

Route Structure and Hierarchy

graph TD
    A[Root] --> B[Public Routes]
    A --> C[Protected Routes]
    
    B --> D[/login]
    B --> E[/register]
    B --> F[/forgot-password]
    
    C --> G[/chats]
    C --> H[/chats/:chatId]
    C --> I[/profile]
    C --> J[/settings]

The chat application will have a protected route structure with:

  • Public Routes: /login, /register, /forgot-password - for authentication flows
  • Protected Routes: /chats, /chats/:chatId, /profile, /settings - for core chat functionality

Code Splitting Strategy

Route-based code splitting with dynamic imports:

// routes.tsx
import { lazy } from 'react';

const Login = lazy(() => import('pages/Login'));
const Register = lazy(() => import('pages/Register'));
const ForgotPassword = lazy(() => import('pages/ForgotPassword'));
const ChatList = lazy(() => import('pages/ChatList'));
const ChatDetail = lazy(() => import('pages/ChatDetail'));
const Profile = lazy(() => import('pages/Profile'));
const Settings = lazy(() => import('pages/Settings'));

export const routes = [
  {
    path: '/login',
    element: <Login />,
    isPublic: true
  },
  {
    path: '/register',
    element: <Register />,
    isPublic: true
  },
  {
    path: '/forgot-password',
    element: <ForgotPassword />,
    isPublic: true
  },
  {
    path: '/chats',
    element: <ChatList />,
    requiresAuth: true
  },
  {
    path: '/chats/:chatId',
    element: <ChatDetail />,
    requiresAuth: true
  },
  {
    path: '/profile',
    element: <Profile />,
    requiresAuth: true
  },
  {
    path: '/settings',
    element: <Settings />,
    requiresAuth: true
  }
];

Justification: Route-based splitting is preferred as it aligns with the application's logical structure and provides clear separation of concerns. Each route can be independently loaded, reducing initial bundle size.

// AuthGuard.tsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from 'stores/auth';

export const AuthGuard = ({ children }: { children: React.ReactNode }) => {
  const navigate = useNavigate();
  const { isAuthenticated, isLoading } = useAuthStore();

  useEffect(() => {
    if (!isAuthenticated && !isLoading) {
      navigate('/login', { replace: true });
    }
  }, [isAuthenticated, isLoading, navigate]);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  return children;
};

Deep Linking Support and URL Parameter Design

URL structure will support:

  • /chats - List view of all chats
  • /chats/:chatId - Specific chat detail page with chat ID as parameter
  • /chats/:chatId?messageId=123 - Deep linking to specific message within chat
// URL parameter validation
const validateChatId = (chatId: string): boolean => {
  return /^[a-zA-Z0-9-_]+$/.test(chatId);
};

const validateMessageId = (messageId: string): boolean => {
  return /^[0-9]+$/.test(messageId);
};

Browser History Management and Scroll Restoration

// App.tsx - scroll restoration setup
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

const App = () => {
  const location = useLocation();

  useEffect(() => {
    // Restore scroll position when navigating between routes
    window.scrollTo(0, 0);
    
    // Or implement more sophisticated scroll restoration logic
    const savedScrollPosition = sessionStorage.getItem(`scroll-position-${location.pathname}`);
    if (savedScrollPosition) {
      window.scrollTo(0, parseInt(savedScrollPosition));
    }
  }, [location.pathname]);

  return (
    <BrowserRouter>
      <ScrollRestoration />
      {/* ... */}
    </BrowserRouter>
  );
};

// Custom scroll restoration component
const ScrollRestoration = () => {
  const location = useLocation();
  
  useEffect(() => {
    // Save scroll position before navigation
    const handleBeforeUnload = () => {
      sessionStorage.setItem(`scroll-position-${location.pathname}`, window.scrollY.toString());
    };
    
    window.addEventListener('beforeunload', handleBeforeUnload);
    
    return () => {
      window.removeEventListener('beforeunload', handleBeforeUnload);
    };
  }, [location.pathname]);

  return null;
};

The application will implement scroll restoration to maintain user context when navigating between chat lists and individual chat views, ensuring a seamless experience for users who may have scrolled through long message histories.

5. API Integration Layer

API Client Design Pattern

We'll implement a centralized API client using Axios with a custom fetch wrapper for better type safety and error handling. The client will be built as a singleton with request/response interceptors to handle authentication, error normalization, and response transformation.

// api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';

class ApiClient {
  private client: AxiosInstance;

  constructor() {
    this.client = axios.create({
      baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'https://api.example.com',
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json',
      },
    });

    this.setupInterceptors();
  }

  private setupInterceptors() {
    // Request interceptor for auth token injection
    this.client.interceptors.request.use(
      (config) => {
        const token = localStorage.getItem('authToken');
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      (error) => Promise.reject(error)
    );

    // Response interceptor for error handling and normalization
    this.client.interceptors.response.use(
      (response) => response,
      (error) => {
        if (error.response?.status === 401) {
          // Handle token expiration
          localStorage.removeItem('authToken');
          window.location.href = '/login';
        }
        return Promise.reject(error);
      }
    );
  }

  public get<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.client.get<T>(url, config);
  }

  public post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.client.post<T>(url, data, config);
  }

  public put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.client.put<T>(url, data, config);
  }

  public delete<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
    return this.client.delete<T>(url, config);
  }
}

export const apiClient = new ApiClient();

Request/Response Interceptors

Authentication token injection is handled via the request interceptor, while response interception handles error cases like 401 Unauthorized (token expiration) and normalizes responses for consistent handling.

Error Handling and Retry Strategy

We implement a retry mechanism with exponential backoff for transient network errors (5xx, 408, 429) and centralized error handling with user-friendly messages.

// api/errorHandler.ts
export class ApiError extends Error {
  constructor(
    public status: number,
    public message: string,
    public code?: string
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

export const handleApiError = (error: any): ApiError => {
  if (error.response) {
    const { status, data } = error.response;
    return new ApiError(
      status,
      data.message || `API Error: ${status}`,
      data.code
    );
  }
  
  if (error.request) {
    return new ApiError(503, 'Network error - please check your connection');
  }
  
  return new ApiError(500, 'Unexpected error occurred');
};

// Retry logic with exponential backoff
export const retryWithBackoff = async <T>(
  fn: () => Promise<T>,
  retries = 3,
  delay = 1000
): Promise<T> => {
  for (let i = 0; i <= retries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === retries) throw error;
      
      // Only retry on network errors or server errors
      if ([500, 502, 503, 504, 429].includes((error as any).response?.status)) {
        await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i)));
        continue;
      }
      
      throw error;
    }
  }
};

Loading, Error, and Empty States Design

We implement a consistent state management approach using React Query for data fetching with built-in loading, error, and success states. Each API call returns a standardized state object that components can consume.

Data Fetching Strategy

For chat application, we use a hybrid approach:

  • Initial load: CSR (Client-Side Rendering) for immediate UI feedback
  • Real-time updates: Server-sent events or WebSocket connections
  • Background sync: React Query's background refetching for stale data
// api/chatService.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from './client';

// Types
export interface Message {
  id: string;
  content: string;
  senderId: string;
  timestamp: Date;
  status: 'sent' | 'delivered' | 'read';
}

export interface Chat {
  id: string;
  participants: string[];
  lastMessage?: Message;
  unreadCount: number;
  updatedAt: Date;
}

// Query keys
const CHAT_QUERY_KEYS = {
  CHATS: ['chats'],
  MESSAGES: (chatId: string) => ['messages', chatId],
  CHAT: (chatId: string) => ['chat', chatId],
};

// Data fetching functions
export const fetchChats = async (): Promise<Chat[]> => {
  const response = await apiClient.get<Chat[]>('/chats');
  return response.data.map(chat => ({
    ...chat,
    updatedAt: new Date(chat.updatedAt)
  }));
};

export const fetchMessages = async (chatId: string): Promise<Message[]> => {
  const response = await apiClient.get<Message[]>(`/chats/${chatId}/messages`);
  return response.data.map(msg => ({
    ...msg,
    timestamp: new Date(msg.timestamp)
  }));
};

export const sendMessage = async (chatId: string, content: string): Promise<Message> => {
  const response = await apiClient.post<Message>(`/chats/${chatId}/messages`, {
    content,
  });
  return response.data;
};

// React Query hooks
export const useChats = () => {
  return useQuery<Chat[], Error>({
    queryKey: CHAT_QUERY_KEYS.CHATS,
    queryFn: fetchChats,
    staleTime: 5 * 60 * 1000, // 5 minutes
    cacheTime: 10 * 60 * 1000, // 10 minutes
  });
};

export const useMessages = (chatId: string) => {
  return useQuery<Message[], Error>({
    queryKey: CHAT_QUERY_KEYS.MESSAGES(chatId),
    queryFn: () => fetchMessages(chatId),
    enabled: !!chatId,
    staleTime: 2 * 60 * 1000, // 2 minutes
  });
};

export const useSendMessage = () => {
  const queryClient = useQueryClient();
  
  return useMutation<Message, Error, { chatId: string; content: string }>({
    mutationFn: ({ chatId, content }) => sendMessage(chatId, content),
    onSuccess: (message, { chatId }) => {
      // Update messages cache
      queryClient.setQueryData<Message[]>(CHAT_QUERY_KEYS.MESSAGES(chatId), (old) => [
        ...(old || []),
        message,
      ]);
      
      // Update chat list with new last message
      queryClient.setQueryData<Chat[]>(CHAT_QUERY_KEYS.CHATS, (old) => {
        return old?.map(chat => 
          chat.id === chatId 
            ? { ...chat, lastMessage: message } 
            : chat
        );
      });
    },
  });
};

API Response Transformation Layer

We implement a DTO-to-domain model mapping layer to normalize API responses into consistent domain objects:

// api/mappers.ts
import { Message, Chat } from './chatService';

export interface MessageDTO {
  id: string;
  content: string;
  sender_id: string;
  timestamp: string;
  status: 'sent' | 'delivered' | 'read';
}

export interface ChatDTO {
  id: string;
  participants: string[];
  last_message?: MessageDTO;
  unread_count: number;
  updated_at: string;
}

export const mapMessageFromDTO = (dto: MessageDTO): Message => ({
  id: dto.id,
  content: dto.content,
  senderId: dto.sender_id,
  timestamp: new Date(dto.timestamp),
  status: dto.status,
});

export const mapChatFromDTO = (dto: ChatDTO): Chat => ({
  id: dto.id,
  participants: dto.participants,
  lastMessage: dto.last_message ? mapMessageFromDTO(dto.last_message) : undefined,
  unreadCount: dto.unread_count,
  updatedAt: new Date(dto.updated_at),
});

Sequence Diagram for Primary Data Flows

sequenceDiagram
    participant U as User
    participant C as Chat Component
    participant Q as React Query
    participant A as API Client
    participant S as Server

    U->>C: Select chat
    C->>Q: Fetch messages for chat
    Q->>A: GET /chats/{chatId}/messages
    A->>S: Request messages
    S-->>A: Response with messages
    A-->>Q: Return messages
    Q-->>C: Render messages

    U->>C: Send message
    C->>Q: Mutate message
    Q->>A: POST /chats/{chatId}/messages
    A->>S: Create new message
    S-->>A: Return created message
    A-->>Q: Update cache
    Q-->>C: Update UI with new message

    U->>C: Fetch chats list
    C->>Q: Fetch chats
    Q->>A: GET /chats
    A->>S: Request chats
    S-->>A: Response with chats
    A-->>Q: Return chats
    Q-->>C: Render chat list

6. Performance Design

Core Web Vitals Targets

  • Largest Contentful Paint (LCP): <2.5s for chat messages and media
  • Interaction to Next Paint (INP): <200ms for chat interactions (typing, sending, scrolling)
  • Cumulative Layout Shift (CLS): <0.1 for chat UI elements and message content
  • First Contentful Paint (FCP): <1.8s for initial chat view rendering

Bundle Size Budget per Route

  • Chat List View: <200KB (including React, routing, basic state management)
  • Chat Detail View: <350KB (includes message list virtualization, media handling)
  • Media Attachment Flow: <150KB (image preview, upload handling)

Critical Rendering Path Optimization

  • Critical CSS: Inline essential styles for chat header, message list, and input area
  • Async Script Loading: Defer non-critical JavaScript (analytics, third-party widgets)
  • Resource Hints: Preload font assets and critical chat data endpoints
  • Code Splitting: Route-based chunking with dynamic imports for heavy components

Image Optimization Strategy

  • Format Selection: WebP for modern browsers, JPEG fallback for legacy support
  • Lazy Loading: loading="lazy" attribute on all chat media elements
  • Responsive Images: srcSet with multiple sizes for different viewport widths
  • Compression: 85% quality JPEG, 90% quality WebP with progressive loading
  • Placeholder Strategy: Low-quality image placeholders (LQIP) for media content

Font Loading Strategy

  • Font Display: font-display: swap to prevent FOIT (Flash of Invisible Text)
  • Preload Critical Fonts: rel="preload" for chat interface font families
  • Font Subsets: Unicode ranges for specific language support (e.g., Latin, Cyrillic)
  • Fallback Fonts: System font stacks with fallback to serif/sans-serif families

Memoization Strategy

// Memoize expensive computations in chat components
const MessageComponent = React.memo(({ message, currentUser }: Props) => {
  const isCurrentUser = useMemo(() => 
    message.senderId === currentUser.id, 
    [message.senderId, currentUser.id]
  );
  
  const formattedTimestamp = useMemo(() => 
    formatTimestamp(message.timestamp), 
    [message.timestamp]
  );

  return (
    <div className={isCurrentUser ? "sent" : "received"}>
      {message.content}
      <span>{formattedTimestamp}</span>
    </div>
  );
});

const ChatList = React.memo(({ chats, onChatSelect }: Props) => {
  const sortedChats = useMemo(() => 
    [...chats].sort((a, b) => 
      new Date(b.lastMessageAt).getTime() - new Date(a.lastMessageAt).getTime()
    ), 
    [chats]
  );

  return (
    <div>
      {sortedChats.map(chat => (
        <ChatItem 
          key={chat.id} 
          chat={chat} 
          onClick={() => onChatSelect(chat)}
        />
      ))}
    </div>
  );
});

Virtualization for Large Lists

// Using react-window for efficient rendering of large chat message lists
import { FixedSizeList as List } from 'react-window';

const VirtualizedMessageList = ({ messages, currentUser }: Props) => {
  const itemSize = 80; // Approximate height of each message item
  
  return (
    <List
      height={500}
      itemCount={messages.length}
      itemSize={itemSize}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          <MessageItem 
            message={messages[index]} 
            currentUser={currentUser}
          />
        </div>
      )}
    </List>
  );
};

Prefetching and Preloading Strategy

  • Chat List Prefetch: Fetch recent 50 chats on app load or when user navigates to chat view
  • Media Preload: Preload first 10 media messages in active chat when entering view
  • User Profile Preload: Fetch user profiles for 20 most recent chat participants
  • Typing Indicator: Debounced prefetch of typing status for active chats (500ms delay)
  • Scroll Position Restore: Cache and restore scroll position for chat history when switching between chats
// Prefetch strategy in chat context
const useChatPrefetch = (chatId: string) => {
  const [prefetchStatus, setPrefetchStatus] = useState<'idle' | 'loading' | 'complete'>('idle');
  
  useEffect(() => {
    if (chatId) {
      setPrefetchStatus('loading');
      
      // Fetch recent messages and user profiles
      Promise.all([
        fetchRecentMessages(chatId),
        fetchParticipantProfiles(chatId)
      ]).then(() => {
        setPrefetchStatus('complete');
      });
    }
  }, [chatId]);
  
  return prefetchStatus;
};

7. Error Handling & Resilience

Error Boundary Placement Strategy

graph TD
    A[ChatApp] --> B[ErrorBoundary]
    B --> C[ChatContainer]
    C --> D[MessageList]
    C --> E[MessageInput]
    C --> F[ChatHeader]
    B --> G[UserList]
    B --> H[OnlineStatus]

Error boundaries will wrap the main chat components to prevent cascading failures:

  • ChatContainer error boundary wraps all chat UI components
  • MessageList has its own boundary for list rendering errors
  • UserList has dedicated error handling for member fetch failures

Global Error Handler Design

// src/core/errorHandler.ts
import { captureException } from '@sentry/react';

export interface ErrorHandlerConfig {
  logToConsole?: boolean;
  showUserFriendlyMessage?: boolean;
  captureToSentry?: boolean;
  retryAttempts?: number;
}

export class GlobalErrorHandler {
  private static instance: GlobalErrorHandler;
  
  private constructor() {}
  
  static getInstance(): GlobalErrorHandler {
    if (!GlobalErrorHandler.instance) {
      GlobalErrorHandler.instance = new GlobalErrorHandler();
    }
    return GlobalErrorHandler.instance;
  }

  handleError(
    error: Error,
    context: string,
    config: ErrorHandlerConfig = {}
  ): void {
    const {
      logToConsole = true,
      showUserFriendlyMessage = true,
      captureToSentry = true,
      retryAttempts = 3
    } = config;

    if (logToConsole) {
      console.error(`[ChatApp] ${context}:`, error);
    }

    if (captureToSentry) {
      captureException(error, {
        tags: { context, component: 'ChatApp' },
        extra: { timestamp: Date.now() }
      });
    }

    if (showUserFriendlyMessage) {
      this.showUserNotification(error.message);
    }
  }

  private showUserNotification(message: string): void {
    // Toast notification system
    const toast = document.createElement('div');
    toast.className = 'chat-toast error';
    toast.textContent = message;
    document.body.appendChild(toast);
    
    setTimeout(() => {
      toast.remove();
    }, 5000);
  }
}

Offline / Network Failure Handling

// src/hooks/useNetworkStatus.ts
import { useState, useEffect } from 'react';

export const useNetworkStatus = () => {
  const [isOnline, setIsOnline] = useState<boolean>(true);
  const [lastReconnectAttempt, setLastReconnectAttempt] = useState<number | null>(null);

  useEffect(() => {
    const handleOnline = () => {
      setIsOnline(true);
      setLastReconnectAttempt(Date.now());
    };

    const handleOffline = () => {
      setIsOnline(false);
    };

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return { isOnline, lastReconnectAttempt };
};

// src/components/OfflineBanner.tsx
interface OfflineBannerProps {
  isOnline: boolean;
  lastReconnectAttempt: number | null;
}

export const OfflineBanner = ({ isOnline, lastReconnectAttempt }: OfflineBannerProps) => {
  if (isOnline) return null;

  return (
    <div className="offline-banner">
      <span className="offline-icon">⚠️</span>
      <span>Offline - Messages will be sent when connection is restored</span>
      {lastReconnectAttempt && (
        <span className="reconnect-time">
          Last attempt: {new Date(lastReconnectAttempt).toLocaleTimeString()}
        </span>
      )}
    </div>
  );
};

Graceful Degradation Patterns

// src/components/ChatFallback.tsx
interface ChatFallbackProps {
  error?: Error;
  retry?: () => void;
  isOffline?: boolean;
}

export const ChatFallback = ({ error, retry, isOffline }: ChatFallbackProps) => {
  if (isOffline) {
    return (
      <div className="chat-fallback offline">
        <h3>Offline Mode</h3>
        <p>You're currently offline. Your messages will be sent when you reconnect.</p>
        <div className="offline-indicator">
          <span>●</span> Offline
        </div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="chat-fallback error">
        <h3>Something went wrong</h3>
        <p>We couldn't load your chat messages. Please try again.</p>
        <button onClick={retry} className="retry-button">
          Retry
        </button>
      </div>
    );
  }

  return (
    <div className="chat-fallback loading">
      <div className="spinner"></div>
      <p>Loading chat...</p>
    </div>
  );
};

User Feedback on Errors

// src/components/ToastManager.tsx
import { useState, useEffect } from 'react';

interface Toast {
  id: string;
  message: string;
  type: 'success' | 'error' | 'warning' | 'info';
  duration?: number;
}

export const ToastManager = () => {
  const [toasts, setToasts] = useState<Toast[]>([]);

  const addToast = (message: string, type: Toast['type'] = 'info', duration = 5000) => {
    const id = Date.now().toString();
    setToasts(prev => [...prev, { id, message, type, duration }]);
    
    setTimeout(() => {
      removeToast(id);
    }, duration);
  };

  const removeToast = (id: string) => {
    setToasts(prev => prev.filter(toast => toast.id !== id));
  };

  return (
    <div className="toast-container">
      {toasts.map(toast => (
        <div 
          key={toast.id} 
          className={`toast toast-${toast.type}`}
          role="alert"
        >
          <span>{toast.message}</span>
          <button 
            onClick={() => removeToast(toast.id)}
            aria-label="Close"
            className="toast-close"
          >
            ×
          </button>
        </div>
      ))}
    </div>
  );
};

// src/hooks/useChatToasts.ts
export const useChatToasts = () => {
  const addToast = (message: string, type: 'success' | 'error' | 'warning' | 'info') => {
    // Implementation would integrate with ToastManager
    console.log(`Toast: ${type} - ${message}`);
  };

  const notifyMessageSendFailure = (message: string) => {
    addToast(
      `Failed to send message: ${message}. Message queued for retry.`,
      'warning'
    );
  };

  const notifyConnectionRestored = () => {
    addToast('Connection restored. Messages sent.', 'success');
  };

  return { notifyMessageSendFailure, notifyConnectionRestored };
};

Retry Strategy Implementation

// src/utils/retryStrategy.ts
export interface RetryConfig {
  maxRetries?: number;
  baseDelay?: number;
  exponentialBackoff?: boolean;
  shouldRetry?: (error: Error) => boolean;
}

export const createRetryableRequest = <T>(
  requestFn: () => Promise<T>,
  config: RetryConfig = {}
): Promise<T> => {
  const {
    maxRetries = 3,
    baseDelay = 1000,
    exponentialBackoff = true,
    shouldRetry = (error) => 
      error.name === 'NetworkError' || 
      error.message.includes('timeout') ||
      error.message.includes('500')
  } = config;

  let attempts = 0;
  
  const attemptRequest = (): Promise<T> => {
    return requestFn().catch(error => {
      if (attempts < maxRetries && shouldRetry(error)) {
        attempts++;
        
        const delay = exponentialBackoff 
          ? baseDelay * Math.pow(2, attempts - 1)
          : baseDelay;
        
        return new Promise(resolve => {
          setTimeout(() => {
            resolve(attemptRequest());
          }, delay);
        });
      }
      
      throw error;
    });
  };

  return attemptRequest();
};

Error Boundary Component

// src/components/ErrorBoundary.tsx
import { Component, ErrorInfo } from 'react';

interface ErrorBoundaryProps {
  children: React.ReactNode;
  fallback?: React.ComponentType<{ error: Error; resetError: () => void }>;
}

interface ErrorBoundaryState {
  hasError: boolean;
  error?: Error;
}

export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
    console.error('Chat component error:', error, errorInfo);
  }

  resetError = () => {
    this.setState({ hasError: false, error: undefined });
  };

  render() {
    if (this.state.hasError) {
      const { fallback: FallbackComponent } = this.props;
      
      if (FallbackComponent) {
        return <FallbackComponent error={this.state.error!} resetError={this.resetError} />;
      }
      
      return (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <p>We're sorry, but there was an error in the chat component.</p>
          <button onClick={this.resetError}>Try Again</button>
        </div>
      );
    }

    return this.props.children;
  }
}

8. Testing Strategy

Unit Testing Approach

Component Tests (70% coverage target)

  • ChatList: Test message truncation, unread count display, active chat highlighting
  • MessageInput: Validate input state management, emoji picker integration, send button disabled states
  • MessageItem: Test message content rendering, timestamp formatting, user avatars
  • ChatHeader: Verify participant list display, online status indicators, group chat name rendering

Utility Tests (90% coverage target)

  • Message formatting utilities (timestamp, mentions, links)
  • User presence status calculation logic
  • Message deduplication and ID generation functions

Test Structure

// Example: Component unit test structure
describe('MessageInput', () => {
  const mockSendMessage = jest.fn();
  
  it('should render input field and send button', () => {
    const { getByRole } = render(<MessageInput onSendMessage={mockSendMessage} />);
    expect(getByRole('textbox')).toBeInTheDocument();
    expect(getByRole('button', { name: 'Send' })).toBeInTheDocument();
  });
  
  it('should call send handler when message is submitted', () => {
    const { getByRole, getByText } = render(<MessageInput onSendMessage={mockSendMessage} />);
    const input = getByRole('textbox');
    
    fireEvent.change(input, { target: { value: 'Hello world' } });
    fireEvent.keyPress(input, { key: 'Enter', code: 'Enter' });
    
    expect(mockSendMessage).toHaveBeenCalledWith('Hello world');
  });
});

Integration Testing Strategy

Feature Flow Tests (80% coverage)

  • End-to-end chat message flow: send → receive → display
  • Multi-user chat participation simulation
  • Message editing and deletion workflows

API Integration Tests

  • Mocked API responses for different chat states (empty, populated, error)
  • Real-time message propagation between simulated clients

E2E Testing Scope

Cypress Test Coverage (Primary flows)

  • User authentication and chat channel navigation
  • Real-time message exchange between multiple users
  • Offline mode simulation and reconnection handling
  • Message search and filtering functionality

Test Data Strategy

  • Mock chat data with realistic message history patterns
  • Multi-user conversation scenarios with varying participation levels
  • Edge case messages (empty, very long, special characters)

Visual Regression Testing

Screenshot Strategy

  • Chat interface snapshots at key breakpoints (mobile, tablet, desktop)
  • Message bubble rendering consistency across browsers
  • Theme variant testing (light/dark mode)

Accessibility Testing Automation

Axe-core Integration

  • Run axe-core checks on all chat UI components
  • Verify keyboard navigation through message list
  • Test screen reader compatibility with live region updates

Test Data Management

Fixtures and Mocks

  • Comprehensive chat history fixtures with various message types
  • User profile mock data with different roles and permissions
  • Error response mocks for network failures and authentication issues

Test Environment Isolation

  • Separate test database instances for chat data
  • Mocked WebSocket connections to simulate real-time behavior
  • Isolated test users with unique identifiers for reliable testing

9. Accessibility (A11y)

WCAG 2.1 AA compliance checklist for this feature

The chat application must meet WCAG 2.1 AA standards across all components:

  • Text Contrast: All text must have a contrast ratio of at least 4.5:1 against its background (minimum 3:1 for large text)
  • Keyboard Navigation: Full keyboard accessibility for all interactive elements (focus order follows logical UI flow)
  • Screen Reader Support: ARIA labels and roles must be properly implemented for dynamic content updates
  • Focus Management: Clear focus indicators and proper focus trapping in modals/overlays
  • Alternative Text: All non-text content must have appropriate alt text (e.g., emojis, avatars)
  • Error Identification: Form errors must be programmatically associated with their input fields
  • Time-based Content: No time limits that prevent users from reading content (unless essential)
  • Sufficient Size: Interactive elements must be at least 44px in touch size (or 24px in desktop view)

Keyboard navigation support

All chat functionality must be fully operable via keyboard:

  • Navigation: Tab key moves focus through all interactive elements in logical order (message list → input field → send button)
  • Message Actions: Space/Enter key activates message actions (reactions, replies)
  • Input Handling: Enter key submits messages, Shift+Enter for new line
  • Message Editing: Keyboard shortcuts for editing and canceling messages (e.g., Ctrl+E to edit)
  • Navigation Shortcuts: Keyboard shortcuts for switching between conversations (Ctrl+Shift+Up/Down)

Focus management strategy

stateDiagram-v2
    [*] --> Idle
    Idle --> FocusedInput : User tabs to input
    FocusedInput --> MessageList : User presses Enter or sends message
    MessageList --> Idle : Focus returns to input after send
    FocusedInput --> ConversationList : User presses Escape or tabs out
    ConversationList --> Idle : Focus restored to input

Focus management patterns:

  • Auto-focus: Input field receives focus after message send or conversation switch
  • Focus trapping: In modals (e.g., emoji picker), focus stays within the modal bounds
  • Focus restoration: When returning to chat view, previous input focus is restored
  • Visual indicators: Clear focus rings for keyboard navigation (not hidden by CSS)

ARIA roles and attributes plan

// Example ARIA implementation in chat components
interface MessageProps {
  id: string;
  content: string;
  sender: string;
  timestamp: string;
  isOwnMessage?: boolean;
  reactions?: Reaction[];
}

// Message component ARIA implementation
const Message = ({ 
  id, 
  content, 
  sender, 
  timestamp,
  isOwnMessage = false,
  reactions 
}: MessageProps) => {
  return (
    <div 
      role="article"
      aria-labelledby={`message-${id}-header`}
      aria-describedby={`message-${id}-content`}
      tabIndex={0}
    >
      <div 
        id={`message-${id}-header`}
        role="heading"
        aria-level={3}
      >
        <span>{sender}</span>
        <span>{timestamp}</span>
      </div>
      <div 
        id={`message-${id}-content`}
        role="region"
        aria-label={`Message from ${sender}: ${content}`}
      >
        {content}
      </div>
      {reactions && reactions.length > 0 && (
        <div 
          role="toolbar" 
          aria-label={`Reactions to message ${id}`}
        >
          {reactions.map((reaction) => (
            <button 
              key={reaction.id}
              role="button"
              aria-label={`Reaction ${reaction.emoji} by ${reaction.count} users`}
            >
              {reaction.emoji} {reaction.count}
            </button>
          ))}
        </div>
      )}
    </div>
  );
};

Screen reader compatibility

  • Dynamic content updates: Use aria-live regions for real-time message additions
  • Message status indicators: Screen reader announcements for "delivered", "read" status changes
  • Conversation list: Proper labeling of active vs inactive conversations with ARIA attributes
  • Input fields: Clear labeling and error states that announce to screen readers
  • Status messages: Use aria-status for system notifications and loading states
// Example live region for real-time updates
const LiveRegion = () => (
  <div 
    aria-live="polite"
    aria-atomic="true"
    className="sr-only"
  >
    {/* Messages will be announced here */}
  </div>
);

// Example status announcement component
const StatusAnnouncer = ({ message }: { message: string }) => (
  <div 
    aria-live="assertive"
    aria-atomic="true"
    className="sr-only"
  >
    {message}
  </div>
);

Additional accessibility considerations

  • Colorblind support: Ensure color contrast meets AA standards and avoid color-only communication
  • Zoom compatibility: Chat interface must remain functional at 200% zoom level
  • High contrast mode: Support for operating system high contrast themes
  • Alternative input methods: Support for voice input and switch access devices
  • Language attributes: Proper lang attribute on content blocks for multilingual chats

Keyboard navigation flow

  1. Conversation List: Tab through conversations, Enter to select
  2. Chat View:
    • Tab to message list (focus on first message)
    • Use arrow keys to navigate messages
    • Tab to input field, Enter to send message
  3. Message Actions:
    • Space/Enter to open reaction picker or reply menu
    • Escape to close menus
  4. Emoji Picker:
    • Tab through emoji grid
    • Enter to select, Escape to close

Focus trapping in modals

When modal components (e.g., emoji picker, reaction menu) open:

  • Trap focus within the modal bounds
  • Prevent tabbing out of modal to parent content
  • Return focus to trigger element when modal closes
const useFocusTrap = (isOpen: boolean, ref: React.RefObject<HTMLElement>) => {
  useEffect(() => {
    if (!isOpen) return;

    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Tab') {
        const focusableElements = ref.current?.querySelectorAll(
          'button, input, textarea, select, [tabindex]:not([tabindex="-1"])'
        ) || [];
        
        if (focusableElements.length === 0) return;
        
        const firstElement = focusableElements[0] as HTMLElement;
        const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
        
        if (e.shiftKey && document.activeElement === firstElement) {
          lastElement.focus();
          e.preventDefault();
        } else if (!e.shiftKey && document.activeElement === lastElement) {
          firstElement.focus();
          e.preventDefault();
        }
      }
    };

    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [isOpen]);
};

10. Security Considerations

XSS Prevention Strategy

Input Sanitization: All user-generated content must be sanitized before rendering to prevent XSS attacks. Implement a robust HTML sanitizer using libraries like DOMPurify or similar.

// Example implementation in a chat message component
import DOMPurify from 'dompurify';

interface ChatMessage {
  id: string;
  content: string;
  sender: string;
  timestamp: Date;
}

const sanitizeMessageContent = (content: string): string => {
  return DOMPurify.sanitize(content, {
    ALLOWED_TAGS: ['b', 'i', 'u', 'em', 'strong'],
    ALLOWED_ATTR: [],
    KEEP_CONTENT: true,
  });
};

const ChatMessageComponent = ({ message }: { message: ChatMessage }) => {
  const sanitizedContent = sanitizeMessageContent(message.content);
  
  return (
    <div 
      className="message-content"
      dangerouslySetInnerHTML={{ __html: sanitizedContent }}
    />
  );
};

CSRF Protection

Token-based Protection: Implement CSRF tokens for all chat-related API requests to prevent cross-site request forgery attacks.

// Token management in a chat context
const useCSRFToken = () => {
  const [csrfToken, setCsrfToken] = useState<string | null>(null);
  
  useEffect(() => {
    // Fetch CSRF token on component mount or session initialization
    const fetchCSRFToken = async () => {
      try {
        const response = await fetch('/api/csrf-token', {
          method: 'GET',
          credentials: 'include',
        });
        const data = await response.json();
        setCsrfToken(data.token);
      } catch (error) {
        console.error('Failed to fetch CSRF token:', error);
      }
    };
    
    fetchCSRFToken();
  }, []);
  
  return csrfToken;
};

// Usage in API calls
const sendChatMessage = async (message: string, csrfToken: string) => {
  const response = await fetch('/api/chat/messages', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrfToken,
    },
    body: JSON.stringify({ content: message }),
  });
  
  return response.json();
};

Content Security Policy (CSP) Headers

Strict CSP Implementation: Configure strict CSP headers to prevent injection attacks and restrict external resource loading.

Content-Security-Policy: 
  default-src 'self'; 
  script-src 'self' 'unsafe-inline' 'unsafe-eval'; 
  style-src 'self' 'unsafe-inline'; 
  img-src 'self' data: https:; 
  font-src 'self' https:; 
  connect-src 'self' wss:; 
  media-src 'self'; 
  frame-ancestors 'none';

Sensitive Data Handling in Frontend State

Data Protection: Never store sensitive user information (passwords, tokens) in frontend state or local storage.

// Secure session management approach
interface ChatSession {
  userId: string;
  sessionId: string;
  token: string; // Should be handled via secure HTTP-only cookies
  lastActive: Date;
}

// Secure token handling - never store in localStorage or sessionStorage
const secureTokenStorage = {
  // Use HTTP-only cookies for session tokens
  setSecureToken: (token: string) => {
    // Set via server-side cookie with secure flags
    document.cookie = `chat_token=${token}; Secure; HttpOnly; SameSite=Strict`;
  },
  
  getSecureToken: (): string | null => {
    // Read from server-side cookie (not accessible via JS)
    const match = document.cookie.match(/chat_token=([^;]+)/);
    return match ? match[1] : null;
  },
  
  clearSecureToken: () => {
    document.cookie = 'chat_token=; Max-Age=0; Secure; HttpOnly; SameSite=Strict';
  }
};

Input Sanitization Approach

Comprehensive Validation: Implement both client-side and server-side validation with strict sanitization rules.

// Input validation and sanitization utilities
const validateAndSanitizeInput = (input: string): string => {
  // Length validation
  if (input.length > 10000) {
    throw new Error('Message too long');
  }
  
  // Remove potentially dangerous patterns
  const dangerousPatterns = [
    /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
    /on\w+\s*=\s*["'][^"']*["']/gi,
    /javascript:/gi,
    /vbscript:/gi,
  ];
  
  let sanitized = input;
  dangerousPatterns.forEach(pattern => {
    sanitized = sanitized.replace(pattern, '');
  });
  
  // Escape HTML entities
  return sanitized
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
};

// Chat message validation hook
const useChatInputValidation = () => {
  const [errors, setErrors] = useState<string[]>([]);
  
  const validateMessage = (message: string): boolean => {
    const newErrors: string[] = [];
    
    if (!message.trim()) {
      newErrors.push('Message cannot be empty');
    }
    
    if (message.length > 1000) {
      newErrors.push('Message exceeds maximum length');
    }
    
    if (message.match(/<[^>]*>/)) {
      newErrors.push('HTML tags not allowed');
    }
    
    setErrors(newErrors);
    return newErrors.length === 0;
  };
  
  return { validateMessage, errors };
};

Additional Security Measures

Rate Limiting: Implement client-side rate limiting to prevent spamming and DDOS attacks.

Authentication: Ensure all chat operations require proper authentication tokens and session validation.

Secure WebSocket Connections: Use WSS (WebSocket Secure) for real-time chat communications to prevent man-in-the-middle attacks.

Audit Logging: Log security-relevant events (failed authentication attempts, message modifications) for monitoring and incident response.

Secure Communication: All chat data should be transmitted over HTTPS with proper TLS configuration to prevent eavesdropping and data interception.

Data Encryption: For sensitive chat content, implement end-to-end encryption where appropriate, ensuring that only intended recipients can read messages.

Access Control: Implement proper authorization checks to ensure users can only access chat rooms they're members of and cannot manipulate others' messages or data.

11. Internationalization (i18n)

Translation Key Structure and Naming Convention

Translation keys will follow a hierarchical structure using dot notation for clear organization:

chat:
  messages:
    new_message: "New message"
    no_messages: "No messages yet"
  input:
    placeholder: "Type your message..."
    send_button: "Send"
  user:
    online: "Online"
    offline: "Offline"
  timestamp:
    today: "Today"
    yesterday: "Yesterday"

Keys will use kebab-case for consistency and avoid special characters. Keys will be grouped by feature areas (chat, user, timestamp) to enable easy maintenance and scoping.

RTL Support Considerations

RTL support will be implemented through CSS classes and direction properties:

// i18n context provider
interface I18nContextType {
  locale: string;
  direction: 'ltr' | 'rtl';
  t: (key: string, params?: Record<string, unknown>) => string;
  changeLocale: (locale: string) => void;
}

// CSS class for RTL layout
const rtlClass = 'rtl-layout';

RTL layouts will be handled via CSS classes applied to root elements, with specific components like message bubbles and input areas adjusting their direction and alignment accordingly.

Date, Number, Currency Formatting Strategy

Date formatting will use Intl.DateTimeFormat with locale-specific options:

const formatDate = (date: Date, locale: string): string => {
  return new Intl.DateTimeFormat(locale, {
    hour: '2-digit',
    minute: '2-digit',
    month: 'short',
    day: 'numeric'
  }).format(date);
};

const formatRelativeTime = (date: Date, locale: string): string => {
  const now = new Date();
  const diffInMs = now.getTime() - date.getTime();
  
  return new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(
    Math.floor(diffInMs / (1000 * 60 * 60 * 24)),
    'day'
  );
};

Number formatting for message counts and user stats will use Intl.NumberFormat with appropriate locale settings.

Locale Detection and Switching Strategy

Automatic locale detection will use browser navigator.language with fallback to 'en-US':

const detectLocale = (): string => {
  const savedLocale = localStorage.getItem('chat_locale');
  if (savedLocale) return savedLocale;
  
  const browserLang = navigator.language || 'en-US';
  const supportedLocales = ['en-US', 'fr-FR', 'es-ES', 'ar-SA'];
  
  return supportedLocales.find(locale => 
    locale.startsWith(browserLang.split('-')[0])
  ) || 'en-US';
};

const switchLocale = (locale: string) => {
  localStorage.setItem('chat_locale', locale);
  // Trigger re-render with new locale context
  window.location.reload();
};

Locale switching will be available through a settings menu in the chat UI, with a dropdown allowing users to select from supported languages. The application will reload with new locale context to ensure all components render correctly in the selected language.

The i18n implementation will support bidirectional text rendering, proper date/time formatting for different cultures, and maintain consistent user experience across all supported locales while preserving the core chat functionality.

12. Monitoring & Observability

Real User Monitoring (RUM) Metrics to Track

The chat application should capture key RUM metrics to understand user experience and performance:

  • First Contentful Paint (FCP): Target <2s for chat interface rendering
  • Largest Contentful Paint (LCP): Target <2.5s for message list loading
  • Cumulative Layout Shift (CLS): Target <0.1 for chat UI stability
  • First Input Delay (FID): Target <100ms for message sending and typing
  • Chat Message Send Time: Track average time from user click to message appearance
  • Message Load Latency: Monitor time to fetch and display initial chat messages
  • Typing Indicator Response Time: Measure delay between user typing and indicator appearance

Error Tracking Integration

Integrate Sentry or similar error tracking service with the following capabilities:

  • Global Error Boundary: Capture unhandled promise rejections and component errors
  • Chat-Specific Error Tracking: Log failed message sends, connection drops, and API failures
  • Contextual Error Metadata: Include user ID, chat room ID, and timestamp in error reports
  • Error Grouping: Aggregate similar errors to identify root causes (e.g., network failures, auth issues)
  • Sentry Performance Monitoring: Track slow API calls and long-running component renders

Custom Events and User Journey Tracking

Implement custom event tracking for key chat interactions:

// Event tracking interface
interface ChatEvent {
  eventName: string;
  userId?: string;
  chatRoomId?: string;
  timestamp: number;
  metadata?: Record<string, any>;
}

// Example event tracking implementation
const trackChatEvent = (event: ChatEvent) => {
  // Send to analytics service or custom telemetry backend
  console.log('Chat Event:', event);
  
  // Example: Track message sent with timing
  if (event.eventName === 'message_sent') {
    const sendTime = Date.now() - event.timestamp;
    // Send to RUM service for performance monitoring
  }
};

Performance Monitoring and Regression Detection

  • Baseline Performance: Establish performance baselines for chat loading, message sending, and typing indicators
  • Automated Regression Testing: Run performance tests in CI pipeline to detect regressions
  • Threshold Alerts: Configure alerts when performance metrics exceed thresholds (e.g., LCP > 3s)
  • User Session Monitoring: Track user session duration and engagement patterns to detect potential issues
  • Network Performance Tracking: Monitor chat message delivery rates and connection stability over time

Telemetry Implementation Strategy

// Telemetry service for chat feature
class ChatTelemetry {
  private readonly telemetryEndpoint = '/api/telemetry';
  
  trackPerformanceMetric(metric: string, value: number, context?: any) {
    const payload = {
      metric,
      value,
      timestamp: Date.now(),
      context: {
        userId: getCurrentUserId(),
        chatRoomId: getCurrentChatRoomId(),
        ...context
      }
    };
    
    // Send to backend or analytics service
    fetch(this.telemetryEndpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    });
  }
  
  trackUserAction(action: string, metadata?: any) {
    this.trackPerformanceMetric(`chat_action_${action}`, 1, metadata);
  }
}

const telemetry = new ChatTelemetry();

Integration with Observability Platforms

  • Datadog/AWS X-Ray: For distributed tracing of chat message flows
  • Grafana/CloudWatch: For dashboard creation and alerting on chat performance metrics
  • Log aggregation: Centralized logging for chat-related operations and errors
  • Custom dashboards: Real-time monitoring of active chat rooms, message rates, and user engagement

Data Privacy Considerations

  • Anonymous User Tracking: Only track aggregate performance metrics without personal identifiers
  • Data Retention Policies: Define how long telemetry data is retained (e.g., 90 days for performance metrics)
  • Compliance: Ensure telemetry data collection complies with GDPR and CCPA requirements
  • Opt-out Mechanism: Provide users with ability to disable performance tracking (if required by regulations)

13. File & Folder Structure

Proposed Directory Structure

/src
  /components
    /chat
      ChatContainer.tsx
      MessageList.tsx
      MessageInput.tsx
      MessageItem.tsx
      ChatHeader.tsx
      UserList.tsx
    /ui
      Button.tsx
      Input.tsx
      Avatar.tsx
      Spinner.tsx
      Modal.tsx
    /layout
      Header.tsx
      Sidebar.tsx
      MainLayout.tsx
  /features
    /chat
      index.ts
      ChatProvider.tsx
      useChat.ts
      chatSlice.ts
    /user
      index.ts
      userSlice.ts
      useUser.ts
  /hooks
    useDebounce.ts
    useScrollToBottom.ts
    useOnlineStatus.ts
  /services
    api.ts
    chatService.ts
    userService.ts
  /types
    index.ts
    chat.ts
    user.ts
  /utils
    formatDate.ts
    validateMessage.ts
    generateId.ts
  /store
    index.ts
    rootReducer.ts
  /routes
    ChatRoutes.tsx
    ProtectedRoute.tsx
  /assets
    /icons
    /images
  App.tsx
  main.tsx

Module Boundaries and Import Rules

  • Feature boundaries: Chat feature is isolated with its own provider, hooks, and slices
  • Shared components: UI primitives like Button, Input, Avatar live in /components/ui and are imported by any feature
  • No cross-feature imports: Chat components don't directly import from user feature, use shared types instead
  • Barrel exports: Each feature has an index.ts that re-exports public APIs for clean imports

Shared vs Feature-Specific Code

Shared code (reusable across features):

  • UI primitives (Button, Input, Avatar)
  • Utility functions (formatDate, generateId)
  • Common hooks (useDebounce, useScrollToBottom)
  • Global types and interfaces

Feature-specific code:

  • Chat-specific components (ChatContainer, MessageList)
  • Chat state management (chatSlice, useChat)
  • API service layer for chat operations
  • Feature-specific hooks and providers

This structure follows a feature-based organization that scales well for large teams while maintaining clear boundaries between features and shared components.

14. Dependencies & Third-Party Libraries

Key Libraries Selected with Justification

1. React Query (TanStack Query)

  • Justification: Excellent for server state management with built-in caching, automatic refetching, and optimistic updates. Provides robust error handling and loading states out of the box.
  • Bundle size: ~15KB gzipped
  • Alternatives considered: Redux Toolkit, Zustand - both more verbose for server state management

2. Socket.IO Client

  • Justification: Industry-standard WebSocket library with built-in reconnection logic, room support, and event handling. Perfect for real-time chat functionality.
  • Bundle size: ~25KB gzipped
  • Alternatives considered: ws, uWebSockets - Socket.IO provides better ecosystem integration and reliability

3. React Hook Form

  • Justification: High-performance form handling with built-in validation, field-level re-rendering, and error management. Reduces boilerplate for chat input forms.
  • Bundle size: ~10KB gzipped
  • Alternatives considered: Formik - more verbose and less performant for complex forms

4. React Virtualized (react-window)

  • Justification: Efficient virtualization for large chat message lists. Reduces DOM nodes and improves rendering performance.
  • Bundle size: ~5KB gzipped
  • Alternatives considered: react-virtual - slightly larger bundle size, same performance characteristics

5. Date-fns

  • Justification: Modular date formatting with zero dependencies, excellent for chat message timestamps and relative time display.
  • Bundle size: ~2KB gzipped
  • Alternatives considered: Moment.js - massive bundle size, no tree-shaking support

6. React Markdown

  • Justification: Safe rendering of markdown content in chat messages with XSS protection and extensible syntax support.
  • Bundle size: ~12KB gzipped
  • Alternatives considered: marked.js - less secure, more complex configuration

7. Axios

  • Justification: Robust HTTP client with interceptors, request/response transformation, and better error handling than fetch.
  • Bundle size: ~15KB gzipped
  • Alternatives considered: Fetch API - requires more boilerplate for interceptors and error handling

Alternatives Considered and Rejected

Redux Toolkit vs. Zustand

  • Rejected: Redux Toolkit would require more boilerplate for real-time chat state management. Zustand's simplicity and built-in middleware support make it more suitable for this feature.
  • Rationale: Chat state is primarily real-time and user-focused, not complex application-wide state.

Material UI vs. Tailwind CSS

  • Rejected: Material UI would add significant bundle size (50+KB) and complexity for a chat feature that can be styled with utility-first CSS.
  • Rationale: Tailwind provides better performance and more flexible styling for chat UI components.

Emotion vs. Styled Components

  • Rejected: Emotion provides better performance and more predictable rendering for chat UI components.
  • Rationale: Chat messages and UI elements require consistent, performant styling with minimal overhead.

Licensing and Bundle Size Impact Assessment

Total estimated bundle size: ~70-80KB (gzipped) for core dependencies

  • Critical path: React Query + Socket.IO + React Hook Form + react-window = ~55KB
  • Enhanced UX: Date-fns + React Markdown + Axios = ~25KB

Bundle optimization strategies:

  1. Tree-shaking enabled for all libraries
  2. Dynamic imports for non-critical features (e.g., emoji picker)
  3. Code splitting for feature-specific libraries
  4. CDN delivery for common dependencies where appropriate

Security considerations: All libraries are actively maintained with security tracking. Regular audit checks and dependency updates are implemented as part of CI pipeline.

Performance impact: All libraries are optimized for production use with minimal runtime overhead. React Query's built-in caching reduces redundant API calls, and react-window prevents DOM bloat for large message lists.

15. Open Questions, Risks & Trade-offs

Decisions that need product/engineering alignment

  • Real-time vs. polling strategy: Should we use WebSockets or long-polling for message delivery? This decision impacts infrastructure costs, latency expectations, and user experience consistency.
  • Message persistence model: Should messages be stored in-memory only, or persisted to local storage for offline access? This affects data loss resilience and user expectations.
  • Message retention policy: How long should messages be retained in the UI? Should we implement infinite scroll or pagination with message limits?
  • User presence indicators: Should we show real-time online/offline status or rely on last seen timestamps? This impacts the complexity of backend services and user expectations.

Known trade-offs made in this design

  • State management complexity vs. performance: Using a global state manager like Zustand or Redux Toolkit provides better organization for complex interactions but adds overhead. For simpler chat flows, React Context might be sufficient.
  • Bundle size vs. feature richness: Including rich text editing, file attachments, and emoji support increases bundle size significantly. We've chosen to implement core features first with extensibility points for future enhancements.
  • User experience vs. accessibility: Prioritizing modern UI interactions (e.g., drag-and-drop file uploads) over fully accessible alternatives that might require more complex keyboard navigation support.
  • Client-side vs. server-side rendering: Using CSR for real-time chat experiences but potentially sacrificing SEO and initial load performance for users without JavaScript.

Risks and mitigations

  • Message ordering consistency: Risk of messages appearing out of order due to async network operations. Mitigation: Implement message IDs and sequence numbers with server-side ordering guarantees.
  • Offline message handling: Risk of losing messages when users go offline. Mitigation: Implement local queue with retry logic and optimistic updates for immediate UI feedback.
  • Scalability under high load: Risk of message delivery delays or connection drops during peak usage. Mitigation: Design with horizontal scaling in mind, including proper connection pooling and message batching.
  • Security vulnerabilities: Risk of XSS attacks through user-generated content. Mitigation: Implement strict content sanitization and input validation on both client and server sides.

Technical debt acknowledged

  • Limited emoji support: Current implementation only supports basic Unicode emojis. Future technical debt includes supporting custom emoji packs and proper emoji rendering across different platforms.
  • Basic file upload handling: Current design assumes small file uploads with no progress tracking or cancellation support. Future improvements would include chunked uploads and resume capabilities.
  • No message search capability: The current design doesn't support searching through conversation history. This is a known limitation for long conversations and will require additional indexing strategies.
  • Simplified typing indicators: Current implementation only shows basic "user is typing" state. Future enhancements would include more granular typing states and better handling of multiple users typing simultaneously.

Was this article helpful?

Topics in this article

More from Frontend LLD

  • Frontend HLD

    WhatsApp Web Frontend Architecture and Design Patterns

    Comprehensive high-level design document detailing WhatsApp Web's frontend architecture, component structure, and performance optimization strategies for enterprise-scale messaging applications.

  • Frontend LLD

    Live Streaming Dashboard Design and Implementation

    Comprehensive low-level design for a real-time streaming dashboard with concurrent viewer tracking, live metrics, and scalable architecture.

  • Frontend HLD

    Real-Time State Synchronization System Design

    Comprehensive frontend architecture for building scalable real-time state synchronization across distributed client applications with conflict resolution and offline support.

  • Frontend LLD

    Chat UI Design System Architecture

    Comprehensive low-level design for scalable, real-time chat user interface with message handling and UI state management

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.