A comprehensive real-time messaging system with direct messages, group chats, file attachments, reactions, read receipts, and presence tracking.

Table of Contents


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 Authorization header
  • 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
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

  1. Cursor-based pagination - Efficient for large message histories
  2. Database indexes - Optimized for common queries
  3. Lazy loading - Participants and messages loaded on demand
  4. Caching - Consider Redis for presence and typing indicators
  5. 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