Files
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

10 KiB

Chat Service Architecture Documentation

Detailed architecture for Chat Service with E2EE, SignalR, scalability patterns, and AI integration.

Architecture Overview

graph TB
    subgraph "Clients"
        WEB[Web App]
        MOB[Mobile App]
        BOT[Bot Client]
    end
    
    subgraph "Load Balancer"
        LB[NGINX/Traefik]
        SS[Sticky Sessions]
    end
    
    subgraph "Chat Service Instances"
        CS1[Instance 1<br/>SignalR Hub]
        CS2[Instance 2<br/>SignalR Hub]
        CS3[Instance 3<br/>SignalR Hub]
    end
    
    subgraph "Backplane"
        RD[(Redis<br/>Pub/Sub)]
    end
    
    subgraph "Storage"
        PG[(PostgreSQL<br/>Messages)]
        RC[(Redis<br/>Cache)]
    end
    
    subgraph "AI Service"
        AI[OpenAI<br/>GPT-4]
    end
    
    WEB & MOB & BOT --> LB
    LB --> SS --> CS1 & CS2 & CS3
    CS1 & CS2 & CS3 <--> RD
    CS1 & CS2 & CS3 --> PG
    CS1 & CS2 & CS3 --> RC
    CS1 & CS2 & CS3 --> AI
    
    style LB fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:3px
    style RD fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px
    style PG fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
    style AI fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px

End-to-End Encryption (E2EE)

X3DH Key Exchange Protocol

The service implements the Extended Triple Diffie-Hellman (X3DH) protocol for establishing encrypted sessions:

sequenceDiagram
    participant Alice as Alice (Sender)
    participant Server as Chat Server
    participant Bob as Bob (Receiver)
    
    Note over Bob,Server: Bob registers keys
    Bob->>Server: Register Key Bundle<br/>(Identity, SignedPreKey, OneTimePreKeys)
    Server->>Server: Store public keys only
    
    Note over Alice,Server: Alice initiates session
    Alice->>Server: Request Bob's Key Bundle
    Server->>Alice: {IdentityKey, SignedPreKey, OneTimePreKey}
    Alice->>Alice: X3DH → Session Key
    Alice->>Alice: AES-256-GCM encrypt message
    Alice->>Server: Send encrypted message
    Server->>Bob: Deliver encrypted message
    Bob->>Bob: X3DH → Session Key
    Bob->>Bob: AES-256-GCM decrypt message
    
    style Server fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:3px

Key Components

Component Description Storage
Identity Key Long-term Curve25519 key pair Private: Client only
Signed Pre-Key Rotated every 30 days Public: Server
One-Time Pre-Keys Consumed per session Public: Server (deleted after use)
Session Key Derived via X3DH Never stored

Security Properties

  • Forward Secrecy: Compromised keys don't reveal past messages
  • Zero-Knowledge Server: Server cannot decrypt messages
  • Deniability: No cryptographic proof of authorship

Domain Model

Aggregate Roots

erDiagram
    Conversation ||--o{ Message : contains
    Conversation ||--o{ ConversationParticipant : has
    ChatUser ||--o{ Conversation : participates
    ChatUser ||--|| UserKeyBundle : has
    ChatUser ||--o{ OneTimePreKey : owns
    
    Conversation {
        uuid Id PK
        string Name
        string AvatarUrl
        enum Type "Direct|Group"
        uuid CreatorId
        datetime CreatedAt
        datetime LastActivityAt
    }
    
    Message {
        uuid Id PK
        uuid ConversationId FK
        uuid SenderId FK
        string EncryptedContent
        string Nonce
        string AuthTag
        enum Type "Text|Image|File|System"
        enum Status "Sent|Delivered|Read|Failed"
        datetime CreatedAt
    }
    
    ConversationParticipant {
        uuid Id PK
        uuid ConversationId FK
        uuid ChatUserId FK
        enum Role "Owner|Admin|Member"
        datetime JoinedAt
        datetime LastReadAt
    }
    
    ChatUser {
        uuid Id PK
        string IdentityUserId
        string DisplayName
        string AvatarUrl
        enum Status "Online|Away|Offline"
        datetime LastSeenAt
    }
    
    UserKeyBundle {
        string IdentityPublicKey
        string SignedPreKey
        string SignedPreKeySignature
        datetime SignedPreKeyTimestamp
    }
    
    OneTimePreKey {
        uuid Id PK
        int KeyId
        string PublicKey
        bool IsUsed
        datetime CreatedAt
    }

Domain Events

Event Trigger Handler
ConversationCreatedDomainEvent New conversation created Notify participants
MessageSentDomainEvent Message sent Update last_activity, broadcast
MessageDeliveredDomainEvent Message delivered Notify sender
MessageReadDomainEvent Message read Notify sender
UserJoinedRoomDomainEvent User joins room Notify members
UserLeftRoomDomainEvent User leaves room Notify members
TypingDomainEvent User typing Broadcast to room
ChatUserCreatedDomainEvent New chat user Initialize presence
UserKeyBundleUpdatedDomainEvent Keys updated Invalidate cached keys

SignalR Hub Architecture

Connection Lifecycle

sequenceDiagram
    participant Client
    participant Hub as ChatHub
    participant Groups
    participant DB as Database
    participant Redis
    
    Client->>Hub: Connect (JWT Token)
    Hub->>Hub: OnConnectedAsync()
    Hub->>DB: Load user conversations
    Hub->>Groups: AddToGroupAsync(conversationId)
    Hub->>Redis: Publish(UserOnline)
    Hub-->>Client: Connected
    
    Note over Client,Hub: User is now online
    
    Client->>Hub: SendMessage(conversationId, encryptedContent)
    Hub->>DB: Save encrypted message
    Hub->>Redis: Publish(NewMessage)
    Redis-->>Hub: Broadcast to all instances
    Hub->>Groups: Clients.Group(conversationId)
    Hub-->>Client: ReceiveMessage
    
    style Hub fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:3px
    style Redis fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px
    style DB fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px

Hub Methods

public class ChatHub : Hub<IChatHubClient>
{
    // Connection management
    public override Task OnConnectedAsync();
    public override Task OnDisconnectedAsync(Exception? exception);
    
    // Room management
    public Task JoinRoom(Guid conversationId);
    public Task LeaveRoom(Guid conversationId);
    
    // Messaging
    public Task SendMessage(Guid conversationId, string encryptedContent, 
                           string nonce, string? authTag);
    public Task SendTypingIndicator(Guid conversationId, bool isTyping);
    public Task MarkMessageRead(Guid conversationId, Guid messageId);
    
    // AI Integration
    public IAsyncEnumerable<string> StreamAIResponse(Guid conversationId, 
                                                      string prompt);
}

Scalability Architecture

Redis Backplane

graph LR
    subgraph "Instance 1"
        C1[Client A] --> H1[Hub 1]
    end
    
    subgraph "Instance 2"
        C2[Client B] --> H2[Hub 2]
    end
    
    subgraph "Redis"
        CH[Channel: chat.messages]
    end
    
    H1 -->|Publish| CH
    CH -->|Subscribe| H2
    H2 -->|Deliver| C2
    
    style CH fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px
    style H1 fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:2px
    style H2 fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:2px

Scaling Options

Option Pros Cons
Redis Backplane Simple, on-premise Requires Redis cluster management
Azure SignalR Serverless, no sticky sessions Vendor lock-in, cost
Sticky Sessions Simplest Imperfect for failover

AI Integration Flow

sequenceDiagram
    participant User
    participant Hub as ChatHub
    participant AI as AIService
    participant OpenAI
    participant DB as Database
    
    User->>Hub: "@gpt Explain DDD"
    Hub->>DB: Load history (20 messages)
    Hub->>AI: StreamAsync(prompt, history)
    AI->>OpenAI: ChatCompletion (stream: true)
    
    loop Streaming
        OpenAI-->>AI: Token chunk
        AI-->>Hub: yield chunk
        Hub-->>User: ReceiveAIChunk
    end
    
    Hub->>DB: Save AI response
    Hub-->>User: AIResponseComplete
    
    style Hub fill:#2C3E50,color:#ECF0F1,stroke:#34495E,stroke-width:3px
    style AI fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:2px
    style OpenAI fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px

Resiliency Patterns

Automatic Reconnect

stateDiagram-v2
    [*] --> Connected
    Connected --> Disconnected: Connection lost
    Disconnected --> Reconnecting: Auto retry
    Reconnecting --> Connected: Success
    Reconnecting --> Reconnecting: Retry with backoff
    Reconnecting --> Disconnected: Max retries exceeded
    Disconnected --> [*]: User logout

Stateful Reconnect (.NET 8+)

// Server configuration
builder.Services.AddSignalR(options =>
{
    options.StatefulReconnectBufferSize = 32 * 1024; // 32KB
});

// With UseStatefulReconnect
app.MapHub<ChatHub>("/chatHub", options =>
{
    options.AllowStatefulReconnects = true;
});

Deployment Architecture

Kubernetes with Sticky Sessions

apiVersion: apps/v1
kind: Deployment
metadata:
  name: chatservice
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: chatservice
        image: chatservice:latest
        ports:
        - containerPort: 8080
        env:
        - name: REDIS_URL
          valueFrom:
            secretKeyRef:
              name: chat-secrets
              key: redis-url
---
apiVersion: v1
kind: Service
metadata:
  name: chatservice
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-sticky-sessions: "true"
spec:
  type: LoadBalancer
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 3600
  ports:
  - port: 80
    targetPort: 8080

Health Checks

builder.Services.AddHealthChecks()
    .AddNpgSql(connectionString, name: "postgresql")
    .AddRedis(redisConnectionString, name: "redis")
    .AddSignalRHub<ChatHub>(name: "signalr-hub");

References