A comprehensive real-time messaging system with direct messages, group chats, file attachments, reactions, read receipts, and presence tracking.
Table of Contents¶
- Overview
- Architecture
- Database Schema
- REST API Endpoints
- WebSocket API
- Features
- Usage Examples
- Security
- Configuration
Overview¶
The chat system provides: - 1-to-1 Direct Messages - Private conversations between two users - Group Chats - Multi-user conversations with roles (Owner, Admin, Member) - Real-time Messaging - WebSocket/STOMP for instant message delivery - Message Threading - Reply to specific messages - File Attachments - Share images and documents - Emoji Reactions - React to messages with emojis - Read Receipts - Track who has read messages - Typing Indicators - See when others are typing - Presence Status - Online/Away/DND/Offline status
Architecture¶
Components¶
┌─────────────────────────────────────────────────────────────┐
│ Client │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ REST API │ │ WebSocket │ │ SockJS │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
└─────────┼──────────────────┼──────────────────┼─────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Spring Boot Server │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ChatConversationController │ │
│ │ ChatMessageController │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ChatWebSocketHandler (STOMP) │ │
│ │ ChatSessionEventListener │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ChatConversationService │ ChatMessageService │ │
│ │ ChatReactionService │ ChatReadReceiptService │ │
│ │ ChatPresenceService │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Repositories │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ PostgreSQL │
└─────────────────┘
Technology Stack¶
| Component | Technology |
|---|---|
| Protocol | STOMP over WebSocket/SockJS |
| Message Broker | Spring Simple Broker (in-memory) |
| Authentication | JWT tokens |
| Database | PostgreSQL with JSONB |
| ORM | Spring Data JPA / Hibernate |
Database Schema¶
Tables¶
| Table | Description |
|---|---|
chat_conversations |
Chat rooms (direct or group) |
chat_participants |
User membership with roles |
chat_messages |
Individual messages |
chat_message_attachments |
File attachments |
chat_message_reactions |
Emoji reactions |
chat_message_read_receipts |
Read tracking |
chat_user_presence |
Online status |
chat_typing_indicators |
Typing status (transient) |
Entity Relationships¶
Conversation (1) ──── (*) ChatParticipant ──── (1) User
│
└──── (*) ChatMessage ──── (*) ChatMessageAttachment ──── (1) TgFile
│
├──── (*) ChatMessageReaction
├──── (*) ChatMessageReadReceipt
└──── (1) ChatMessage (parent for threading)
REST API Endpoints¶
Conversations¶
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/chat/conversations |
List user's conversations |
GET |
/api/chat/conversations/{id} |
Get conversation details |
POST |
/api/chat/conversations |
Create group conversation |
POST |
/api/chat/conversations/direct/{userId} |
Get/create DM |
PUT |
/api/chat/conversations/{id} |
Update conversation |
DELETE |
/api/chat/conversations/{id} |
Leave conversation |
GET |
/api/chat/conversations/pinned |
Get pinned conversations |
GET |
/api/chat/conversations/search?query= |
Search groups |
GET |
/api/chat/conversations/unread |
Get unread counts |
Participants¶
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/chat/conversations/{id}/participants |
Add participants |
DELETE |
/api/chat/conversations/{id}/participants/{userId} |
Remove participant |
PUT |
/api/chat/conversations/{id}/mute?muted= |
Mute/unmute |
PUT |
/api/chat/conversations/{id}/pin?pinned= |
Pin/unpin |
PUT |
/api/chat/conversations/{id}/archive?archived= |
Archive/unarchive |
Messages¶
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/chat/conversations/{id}/messages |
Get messages (paginated) |
POST |
/api/chat/conversations/{id}/messages |
Send message |
GET |
/api/chat/messages/{id}/thread |
Get thread replies |
PUT |
/api/chat/messages/{id} |
Edit message |
DELETE |
/api/chat/messages/{id} |
Delete message |
GET |
/api/chat/conversations/{id}/messages/search?query= |
Search messages |
GET |
/api/chat/conversations/{id}/attachments |
Get messages with files |
Reactions¶
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/chat/messages/{id}/reactions |
Add reaction |
DELETE |
/api/chat/messages/{id}/reactions/{emoji} |
Remove reaction |
POST |
/api/chat/messages/{id}/reactions/toggle |
Toggle reaction |
GET |
/api/chat/messages/{id}/reactions |
Get reactions |
GET |
/api/chat/messages/{id}/reactions/summary |
Get reaction summary |
Read Receipts¶
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/chat/conversations/{id}/read |
Mark as read |
GET |
/api/chat/messages/{id}/read-receipts |
Get read receipts |
Presence¶
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/chat/presence |
Get online users |
GET |
/api/chat/presence/{userId} |
Get user presence |
PUT |
/api/chat/presence/status |
Update own status |
POST |
/api/chat/presence/users |
Get presence for users |
WebSocket API¶
Connection¶
Connect to WebSocket endpoint with JWT token:
const socket = new SockJS('/ws/chat?token=' + jwtToken);
const stompClient = Stomp.over(socket);
stompClient.connect({
Authorization: 'Bearer ' + jwtToken
}, onConnected, onError);
STOMP Destinations¶
Client → Server (Send)¶
| Destination | Payload | Description |
|---|---|---|
/app/chat.send |
SendMessageRequest |
Send a message |
/app/chat.typing |
{conversationId, typing} |
Typing indicator |
/app/chat.markRead |
{conversationId, lastReadMessageId?} |
Mark as read |
/app/chat.reaction |
{messageId, conversationId, emoji, add} |
Add/remove reaction |
/app/chat.presence |
{online, deviceType?, browserInfo?} |
Update presence |
/app/chat.heartbeat |
(empty) | Keep-alive ping |
Server → Client (Subscribe)¶
| Destination | Payload | Description |
|---|---|---|
/user/queue/chat/messages |
ChatMessageResponse |
New messages |
/user/queue/chat/typing |
TypingIndicatorResponse |
Typing notifications |
/user/queue/chat/presence |
PresenceResponse |
Presence updates |
/user/queue/chat/receipts |
ReadReceiptBroadcast |
Read receipts |
/user/queue/chat/reactions |
ReactionBroadcast |
Reaction updates |
/user/queue/chat/errors |
{error: string} |
Error messages |
Message Payloads¶
SendMessageRequest¶
{
"conversationId": 1,
"content": "Hello!",
"messageType": "TEXT",
"parentMessageId": null,
"attachmentIds": [],
"metadata": {},
"tempId": "temp-123"
}
ChatMessageResponse¶
{
"id": 1,
"conversationId": 1,
"messageType": "TEXT",
"content": "Hello!",
"senderId": 1,
"senderName": "John Doe",
"parentMessageId": null,
"replyCount": 0,
"isEdited": false,
"isDeleted": false,
"attachments": [],
"reactionSummary": {"👍": 2, "❤️": 1},
"createdAt": "2024-01-15T10:30:00Z"
}
Features¶
Direct Messages¶
Create or get existing DM with another user:
curl -X POST http://localhost:1337/api/chat/conversations/direct/2 \
-H "Authorization: Bearer $JWT"
Group Conversations¶
Create a group:
curl -X POST http://localhost:1337/api/chat/conversations \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"name": "Project Team",
"description": "Discussion for Project X",
"participantIds": [2, 3, 4],
"allowReactions": true,
"allowReplies": true,
"allowFileUploads": true
}'
Message Threading¶
Reply to a message:
curl -X POST http://localhost:1337/api/chat/conversations/1/messages \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"content": "I agree with this point",
"parentMessageId": 5
}'
Reactions¶
Toggle a reaction:
curl -X POST http://localhost:1337/api/chat/messages/1/reactions/toggle \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{"emoji": "👍"}'
Cursor-based Pagination¶
Load older messages:
# Get initial messages
curl "http://localhost:1337/api/chat/conversations/1/messages?limit=50"
# Get older messages (before message ID 100)
curl "http://localhost:1337/api/chat/conversations/1/messages?before=100&limit=50"
# Get newer messages (after message ID 100)
curl "http://localhost:1337/api/chat/conversations/1/messages?after=100&limit=50"
Usage Examples¶
JavaScript WebSocket Client¶
class ChatClient {
constructor(token) {
this.token = token;
this.stompClient = null;
}
connect() {
const socket = new SockJS('/ws/chat');
this.stompClient = Stomp.over(socket);
this.stompClient.connect(
{ Authorization: `Bearer ${this.token}` },
() => this.onConnected(),
(error) => this.onError(error)
);
}
onConnected() {
// Subscribe to messages
this.stompClient.subscribe('/user/queue/chat/messages', (message) => {
const msg = JSON.parse(message.body);
console.log('New message:', msg);
});
// Subscribe to typing indicators
this.stompClient.subscribe('/user/queue/chat/typing', (message) => {
const typing = JSON.parse(message.body);
console.log('Typing:', typing);
});
// Subscribe to presence updates
this.stompClient.subscribe('/user/queue/chat/presence', (message) => {
const presence = JSON.parse(message.body);
console.log('Presence:', presence);
});
// Set online status
this.stompClient.send('/app/chat.presence', {}, JSON.stringify({
online: true,
deviceType: 'web'
}));
}
sendMessage(conversationId, content) {
this.stompClient.send('/app/chat.send', {}, JSON.stringify({
conversationId,
content,
messageType: 'TEXT'
}));
}
startTyping(conversationId) {
this.stompClient.send('/app/chat.typing', {}, JSON.stringify({
conversationId,
typing: true
}));
}
stopTyping(conversationId) {
this.stompClient.send('/app/chat.typing', {}, JSON.stringify({
conversationId,
typing: false
}));
}
markAsRead(conversationId) {
this.stompClient.send('/app/chat.markRead', {}, JSON.stringify({
conversationId
}));
}
addReaction(messageId, conversationId, emoji) {
this.stompClient.send('/app/chat.reaction', {}, JSON.stringify({
messageId,
conversationId,
emoji,
add: true
}));
}
disconnect() {
if (this.stompClient) {
this.stompClient.disconnect();
}
}
}
// Usage
const chat = new ChatClient(jwtToken);
chat.connect();
chat.sendMessage(1, 'Hello, everyone!');
Security¶
Authentication¶
- REST API: JWT Bearer token in
Authorizationheader - WebSocket: JWT token in connection headers or query parameter
Authorization¶
| Role | Permissions |
|---|---|
| OWNER | All permissions, delete conversation, transfer ownership |
| ADMIN | Add/remove members, edit settings (except delete) |
| MEMBER | Send messages, react, leave conversation |
Rate Limiting (Recommended)¶
| Action | Limit |
|---|---|
| Send message | 30/minute |
| Upload file | 10/minute |
| Add reaction | 60/minute |
Validation¶
- Maximum message length: 10,000 characters
- Maximum file size: Configurable per conversation (default 25MB)
- Allowed file types: Configurable
- XSS prevention: Content sanitization
Configuration¶
Application Properties¶
# WebSocket settings
spring:
websocket:
message-broker:
relay:
enabled: false # Use simple broker
# Chat settings (custom)
app:
chat:
max-message-length: 10000
max-file-size-mb: 25
typing-indicator-ttl-seconds: 5
presence-stale-threshold-minutes: 5
Database Migration¶
The chat system uses Flyway migration V34__chat_system.sql which:
- Creates all required tables
- Sets up indexes for performance
- Creates triggers for automatic stats updates
- Adds constraints for data integrity
Webhook Events¶
The chat system triggers the following webhook events:
| Event | Description |
|---|---|
chat_message.sent |
When a message is sent |
chat_message.updated |
When a message is edited |
chat_message.deleted |
When a message is deleted |
chat_conversation.created |
When a conversation is created |
chat_participant.added |
When a user joins |
chat_participant.removed |
When a user leaves |
Error Handling¶
REST API Errors¶
{
"success": false,
"error": "You are not a participant in this conversation",
"data": null
}
WebSocket Errors¶
Errors are sent to /user/queue/chat/errors:
{
"error": "Failed to send message: Conversation not found"
}
Performance Considerations¶
- Cursor-based pagination - Efficient for large message histories
- Database indexes - Optimized for common queries
- Lazy loading - Participants and messages loaded on demand
- Caching - Consider Redis for presence and typing indicators
- Connection pooling - For WebSocket connections at scale
Future Enhancements¶
- [ ] External message broker (RabbitMQ/Redis) for horizontal scaling
- [ ] Message search with Elasticsearch
- [ ] Push notifications integration
- [ ] Voice/video call support
- [ ] Message encryption (E2E)
- [ ] Bot/automation support