10 KiB
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");