A real-time chat application built with Spring Boot backend and Vue.js frontend, featuring WebSocket-based instant messaging, group conversations, and scalable multi-instance architecture.
- Framework: Spring Boot 4.0.1 (Java 21)
- Database: MongoDB 7.0+ (primary data store)
- Cache & Pub/Sub: Redis 7.x (real-time event distribution)
- Object Storage: MinIO (media file storage)
- Authentication: JWT (HS384) with refresh tokens
- WebSocket: STOMP over WebSocket
- Email: Spring Mail (password reset flow)
- Framework: Vue 3 (Composition API)
- State Management: Pinia
- Routing: Vue Router
- HTTP Client: Axios
- WebSocket: @stomp/stompjs
- UI: Custom CSS with theme support (light/dark mode)
- Build Tool: Vite
The system follows a hybrid REST + WebSocket architecture where:
- REST API: Handles all write operations and queries
- WebSocket: Delivers real-time events to connected clients
- MongoDB: Single source of truth for all persistent data
- Redis Pub/Sub: Real-time message fanout across backend instances
The backend is designed for horizontal scalability. Multiple backend instances coordinate through:
- Shared MongoDB: All instances read/write to the same database
- Redis Pub/Sub: Events published by one instance are received by all instances
- Stateless Design: No instance-specific state; any instance can serve any request
[Client A] ──WebSocket──> [Backend Instance 1] ──┐
│
[Client B] ──WebSocket──> [Backend Instance 2] ──┼──> [MongoDB]
│
[Client C] ──WebSocket──> [Backend Instance 3] ──┘
│
└──────> [Redis Pub/Sub] ────> Fan-out to all instances
POST /api/messages
{
"conversationId": "conv123",
"content": "Hello World"
}
Backend Processing:
- Authentication: JWT validated, user identity extracted from security context
- Authorization: Check if user is a member of the conversation
- Persistence: Message saved to MongoDB with server-generated timestamp
- Redis Publication: Message event published to
conversation:conv123:messageschannel - Response: HTTP 200 returned immediately after Redis publish (async)
- All backend instances subscribe to
conversation:*:messagespattern - Each instance receives the published message event
- Instances filter events to match their connected clients
- Each backend instance iterates through its active WebSocket sessions
- For clients subscribed to
/topic/conversation.conv123, the message is sent via STOMP - Clients receive message in real-time without polling
Client → Backend: GET /ws (with JWT in query param ?token=...)
Backend: Validate JWT signature and expiration
Backend: Extract userId from JWT subject claim
Backend: Upgrade connection to WebSocket
Backend: Store session mapping in Redis
Client → Backend: SUBSCRIBE /topic/conversation.conv123
Backend: Check if authenticated user is member of conv123
Backend: If authorized, allow subscription
Backend: If unauthorized, send STOMP ERROR frame and reject
Client disconnects
Backend: Remove session from Redis presence tracking
Backend: Remove all subscriptions for that session
- Client sends username, password, displayName
- Backend hashes password with BCrypt
- User document saved to MongoDB
- Access token (JWT, 1 hour expiry) and refresh token (random, 30 days expiry) generated
- Refresh token persisted to MongoDB
- Both tokens returned to client
- Client sends username + password
- Backend loads user from MongoDB
- BCrypt verifies password
- New access token + refresh token generated
- Old refresh tokens remain valid (no revocation)
- Client sends refresh token (via HTTP-only cookie)
- Backend queries MongoDB for token validity
- If valid and not revoked/expired, new access token issued
- Refresh token is NOT rotated
- Client sends logout request
- Backend marks refresh token as revoked in MongoDB
- Access token remains valid until natural expiration (cannot be revoked)
Membership-Based Access Control:
- Authorization is enforced through
ConversationMembercollection - Each member has:
role: "OWNER" or "MEMBER"canSendMessage: Boolean flag for write permission
- Read Access: Membership existence is sufficient
- Write Access: Membership +
canSendMessage = true - Owner Privileges: Role "OWNER" can modify conversation metadata
Enforcement Layers:
- REST Controllers: Validate membership before executing service methods
- WebSocket Interceptors: Validate membership before allowing subscriptions
- Both layers independently check the same repository method
The backend publishes events to Redis Pub/Sub for real-time delivery:
| Event Type | Redis Channel | Purpose |
|---|---|---|
| New Message | conversation:{id}:messages |
Message created |
| Message Update | user:{id}:message-updates |
Message edited/deleted |
| Unread Count | user:{id}:unread |
Unread counter changed |
| Seen Receipt | user:{id}:seen |
User marked conversation as read |
| Typing Indicator | user:{id}:typing |
User started/stopped typing (5s TTL) |
| Reactions | user:{id}:reactions |
Reaction added/removed |
| Group Events | user:{id}:group-events |
Member added/removed/role changed |
Session Lifecycle:
Connect:
SET session:{sessionId}:user {userId}
SADD presence:user:{userId} {sessionId}
Disconnect:
GET session:{sessionId}:user → {userId}
DEL session:{sessionId}:user
SREM presence:user:{userId} {sessionId}
Check Online:
EXISTS presence:user:{userId}
or SCARD presence:user:{userId} > 0
Typing Indicators (Ephemeral):
Start Typing:
SETEX typing:{conversationId}:{userId} 5 "true"
Stop Typing:
DEL typing:{conversationId}:{userId}
Auto-Expire: 5 seconds
- MongoDB is Source of Truth: All persistent data stored in MongoDB. Redis contains only ephemeral state.
- WebSocket is Read-Only: No data persistence via WebSocket. All writes via REST.
- User Identity from JWT: Never trust client-supplied user IDs. Always extract from security context.
- Membership Checked Everywhere: Authorization enforced in both REST and WebSocket layers.
- Async Pub/Sub: REST responses return after MongoDB save + Redis publish (non-blocking).
┌─────────────────────────────────────────────────────┐
│ Load Balancer │
│ (HTTP + WebSocket Support) │
└───────────┬─────────────────────────┬────────────────┘
│ │
┌───────────▼────────┐ ┌───────────▼────────┐
│ Backend Instance │ │ Backend Instance │
│ (Port 8080) │ │ (Port 8080) │
└─────┬──────────┬───┘ └─────┬──────────┬───┘
│ │ │ │
│ ┌──────▼───────────────▼──────┐ │
│ │ Redis (Pub/Sub) │ │
│ │ Port 6379 (No persistence) │ │
│ └─────────────────────────────┘ │
│ │
┌──▼──────────────────────────────────────▼───┐
│ MongoDB Replica Set │
│ Port 27017 (Primary Data Store) │
└────────────────────────────────────────────┘
│
┌──────────▼──────────┐
│ MinIO │
│ Object Storage │
│ Port 9000 │
└─────────────────────┘
Backend (application.yml or environment):
jwt.secret-key: <min 32 chars HMAC secret>
jwt.access-token-validity: 3600000 # 1 hour in ms
jwt.refresh-token-validity: 2592000000 # 30 days in ms
spring.data.mongodb.uri: mongodb://user:pass@host:27017/chatapp
spring.data.redis.host: redis-host
spring.data.redis.port: 6379
minio.url: http://minio:9000
minio.access-key: <access-key>
minio.secret-key: <secret-key>Frontend (.env):
VITE_API_BASE_URL=http://localhost:8080/api
VITE_WS_URL=ws://localhost:8080/ws
Horizontal Scaling:
- Add backend instances behind load balancer
- All instances share MongoDB + Redis
- Sticky sessions recommended but not required
- Each instance maintains separate WebSocket connections
Connection Limits:
- Bound by OS file descriptors and JVM thread pool
- Typical: 10,000-50,000 concurrent WebSocket connections per instance
- Scale horizontally for higher concurrency
Redis Requirements:
- Pub/Sub only (no persistence needed)
- Redis Cluster supported but not required
- Replication (master-replica) improves availability
MongoDB Requirements:
- Replica set recommended for production
- Automatic failover supported
- Indexes auto-created by Spring Data MongoDB
The frontend is a single-page application (SPA) built with Vue 3, featuring:
- Real-time Messaging: WebSocket-based instant message delivery
- Conversation Management: Direct messages and group conversations
- Authentication: JWT-based with automatic token refresh
- Theme Support: Light/dark mode with persistent preference
- Responsive Design: Mobile-friendly UI
- Profile Management: Avatar upload, display name editing
- auth: User authentication, login/logout, token management
- conversations: Conversation list, active conversation selection
- messages: Message history, sending, editing, deletion
- realtime: WebSocket connection, typing indicators, presence
- blocking: User blocking functionality
// Connection established with JWT
stompClient.connect({ token: accessToken }, () => {
// Subscribe to conversation topics
stompClient.subscribe('/topic/conversation.{id}', handleMessage)
stompClient.subscribe('/user/queue/unread', handleUnread)
stompClient.subscribe('/user/queue/typing', handleTyping)
})- Scroll Throttling: requestAnimationFrame for smooth scrolling
- Selective Reactivity: Removed deep watchers on message arrays
- Lifecycle Cleanup: Properly unsubscribe and clear timers on unmount
- Direct Mutations: Optimized store updates to prevent cascading re-renders
- Testcontainers: Real MongoDB + Redis instances in Docker
- End-to-End Flow: REST → MongoDB → Redis → WebSocket delivery
- Security Tests: JWT validation, subscription authorization
Run Tests:
cd backend
mvn test- Open multiple browser tabs with different users
- Send messages and verify real-time delivery
- Test conversation switching, typing indicators, seen receipts
- Java 21+
- Node.js 18+
- Docker + Docker Compose
- MongoDB 7.0+
- Redis 7.x
- Start Infrastructure:
docker-compose up -d # MongoDB, Redis, MinIO- Backend:
cd backend
mvn spring-boot:run- Frontend:
cd frontend
npm install
npm run dev- Access:
- Frontend:
http://localhost:5173 - Backend API:
http://localhost:8080/api - WebSocket:
ws://localhost:8080/ws
- Stateless JWT Authentication: No server-side sessions
- Membership-Based Authorization: Every operation checks conversation membership
- HTTP-only Refresh Tokens: Protected from XSS
- BCrypt Password Hashing: Industry-standard (10 rounds)
- CORS Configuration: Restricted frontend origins
- WebSocket Authentication: JWT validated during handshake
- Subscription Authorization: Membership checked before allowing topic subscriptions
- Access Tokens: Stateless JWTs, cannot be revoked, short-lived (1 hour)
- Refresh Tokens: Stored in MongoDB, can be explicitly revoked, long-lived (30 days)
curl http://localhost:8080/actuator/health- Active WebSocket connections per instance
- Redis Pub/Sub publish failures
- MongoDB connection pool utilization
- Message delivery latency
- Presence tracking set sizes in Redis
| Scenario | Impact | Recovery |
|---|---|---|
| Backend Instance Crash | Clients reconnect to other instances | Automatic (load balancer) |
| Redis Down | No real-time delivery | Messages persist in MongoDB, clients poll |
| MongoDB Down | All operations fail | Requires manual intervention |
| Client Disconnect | Session removed from Redis | Client reconnects automatically |
Feel free to clone this project and make your own changes. For questions or issues, please open a GitHub issue or contact me directly.

