WebSocket / STOMP
Mục tiêu
Thiết lập kênh realtime để Backend có thể đẩy thông báo và sự kiện listing tới client đã đăng nhập mà không cần polling liên tục. Trang này là nguồn contract duy nhất cho STOMP-over-WebSocket giữa FE/Mobile và BE.
Tổng quan hệ thống
- WebSocket là hạ tầng realtime dùng chung cho notification, listing live-update và các kênh realtime tương lai.
- Giao thức chuẩn hiện tại: STOMP over WebSocket.
- Endpoint native:
/api/ws. - Endpoint fallback SockJS:
/api/ws-sockjs. - Backend dùng simple broker in-memory với broker prefixes
/topic,/queue. - User destination prefix là
/user, dùng cho message riêng từng user. - Auth diễn ra ở STOMP
CONNECT, không dựa vào HTTP handshake.
Phạm vi
Trong phạm vi:
- STOMP-over-WebSocket endpoint native
/api/ws. - SockJS fallback endpoint
/api/ws-sockjs. - JWT access token trong STOMP native header
Authorization: Bearer <token>. - Server push notification mới tới
/user/queue/notifications. - Server push unread count tới
/user/queue/notifications.unread-count. - Server push payment order status tới
/user/queue/payment-orders. - Server broadcast listing event tới
/topic/listings/{listingId}. - Authorization cho
SEND,SUBSCRIBE, admin topic và user queue. - Rate limit inbound WebSocket frame.
- Heartbeat STOMP client/server theo cấu hình runtime.
Ngoài phạm vi:
- Chat realtime, presence, typing indicator hoặc collaborative editing.
- Push notification ngoài app qua FCM/APNS/email/SMS.
- External message broker như RabbitMQ/Redis STOMP relay ở giai đoạn hiện tại.
- Thiết kế UI notification center, badge unread hoặc listing dashboard.
- Offline queue trên client khi mất mạng.
User Stories
- Là user đã đăng nhập, tôi muốn nhận notification realtime để biết ngay khi có sự kiện mới liên quan đến tài khoản.
- Là client app, tôi muốn subscribe unread count realtime để cập nhật badge thông báo mà không polling liên tục.
- Là user đang chờ thanh toán, tôi muốn nhận realtime payment status khi IPN/webhook xác nhận top-up thành công để màn chờ cập nhật ngay.
- Là broker, tôi muốn nhận sự kiện listing realtime để dashboard có thể cập nhật danh sách/tình trạng tin nhanh hơn.
- Là Backend service, tôi muốn publish message qua một facade chung để không phụ thuộc trực tiếp vào chi tiết broker.
- Là hệ thống bảo mật, tôi muốn WebSocket
CONNECTđược xác thực bằng access token và các subscription nhạy cảm được kiểm soát.
Kết nối và xác thực
Client kết nối tới /api/ws hoặc /api/ws-sockjs, sau đó gửi STOMP CONNECT kèm native header:
Authorization: Bearer <accessToken>Quy tắc:
- Token phải là JWT access token hợp lệ.
- Refresh token không được dùng cho WebSocket.
- HTTP upgrade endpoint public ở tầng security, nhưng
CONNECTthiếu/sai token bị reject. - Principal sau khi xác thực dùng
userIdlàmname, để server push được qua/user/queue/**. - Allowed origins đọc từ
APP_WS_ALLOWED_ORIGINShoặc fallbackCORS_ALLOWED_ORIGINS.
Destination contract
| Destination | Loại | Consumer | Mục đích |
|---|---|---|---|
/user/queue/notifications | user queue | FE/Mobile | Nhận NotificationResponse mới của user hiện tại |
/user/queue/notifications.unread-count | user queue | FE/Mobile | Nhận unread count mới nhất |
/user/queue/payment-orders | user queue | FE/Mobile | Nhận PaymentOrderRealtimePayload khi payment order của user đổi trạng thái |
/user/queue/errors | user queue | FE/Mobile | Nhận lỗi STOMP có cấu trúc |
/topic/listings/{listingId} | topic | FE/Mobile | Nhận listing event theo từng listing |
/topic/admin/** | topic | Admin | Kênh realtime admin, yêu cầu role admin |
/app/** | application destination | FE/Mobile -> BE | Prefix dành cho message client gửi lên BE khi có @MessageMapping |
Authorization
- Client đã xác thực được subscribe
/user/queue/**và/topic/**. - Subscribe
/topic/adminhoặc/topic/admin/**yêu cầu role admin. - Client chỉ được
SENDtới/app/**. - Client không được
SENDtrực tiếp vào/topic/**,/queue/**hoặc/user/**. - Inbound frame đi qua rate limit bucket
websocket.
Acceptance Criteria
- Backend cung cấp STOMP WebSocket endpoint
/api/ws. - Backend cung cấp SockJS fallback endpoint
/api/ws-sockjs. - HTTP upgrade cho
/ws/**và/ws-sockjs/**được public ở lớp HTTP security, nhưng STOMPCONNECTbắt buộc có JWT access token hợp lệ. - Client gửi token qua STOMP native header
Authorizationvới formatBearer <accessToken>. - Token dùng cho WebSocket phải là access token, không chấp nhận refresh token hoặc token sai subject.
- Khi JWT hợp lệ, WebSocket principal dùng
userIdlàmnameđể server push tới đúng/user/queue/**. - Broker hỗ trợ destination
/topic/**và/queue/**, application destination prefix là/app. - Client chỉ được
SENDtới destination dưới/app/**; client không được tự gửi vào/topic/**hoặc/queue/**. - Client đã xác thực được subscribe
/topic/**và/user/queue/**. - Subscription tới
/topic/adminhoặc/topic/admin/**yêu cầu role admin. - Server push notification cá nhân tới
/user/queue/notifications. - Server push unread count cá nhân tới
/user/queue/notifications.unread-count. - Server push payment order status cá nhân tới
/user/queue/payment-orderssau khi payment transaction commit. - Server broadcast listing event tới
/topic/listings/{listingId}. - Khi có lỗi xử lý message, server trả lỗi có cấu trúc qua
/user/queue/errorsnếu lỗi đi qua STOMP handler. - WebSocket heartbeat client/server đọc từ cấu hình runtime.
- Allowed origins cho WebSocket đọc từ cấu hình
app.websocket.allowed-origins. - Inbound WebSocket frame đi qua rate limit để giảm rủi ro spam
CONNECT/SEND/SUBSCRIBE.
sequenceDiagram
actor User
participant App as FE/Mobile
participant WS as BE WebSocket
participant Auth as JWT/Auth
participant Broker as STOMP Broker
participant Service as BE Service
User->>App: Mở app sau khi đăng nhập
App->>WS: CONNECT /api/ws hoặc /api/ws-sockjs\nAuthorization: Bearer accessToken
WS->>Auth: Parse JWT, kiểm tra access token và roles
Auth-->>WS: Principal userId + authorities
WS-->>App: CONNECTED
App->>Broker: SUBSCRIBE /user/queue/notifications
App->>Broker: SUBSCRIBE /user/queue/notifications.unread-count
App->>Broker: SUBSCRIBE /user/queue/payment-orders
App->>Broker: SUBSCRIBE /topic/listings/{listingId}
Service->>Broker: Push notification tới user
Broker-->>App: MESSAGE /user/queue/notifications
Service->>Broker: Push unread count mới
Broker-->>App: MESSAGE /user/queue/notifications.unread-count
Service->>Broker: Push payment order status mới
Broker-->>App: MESSAGE /user/queue/payment-orders
Service->>Broker: Broadcast listing event
Broker-->>App: MESSAGE /topic/listings/{listingId}Notification flow
- Module nghiệp vụ publish
NotificationEventsau khi transaction chính commit. - Notification module tạo hoặc cập nhật notification row.
- Backend push notification mới tới
/user/queue/notifications. - Backend tính lại unread count và push tới
/user/queue/notifications.unread-count. - Client có thể gọi REST notification API để reconcile nếu vừa reconnect hoặc nghi ngờ mất message.
Listing event flow
- Listing được tạo/cập nhật hoặc thay đổi trust score.
- Backend xác định listing event payload.
- Backend broadcast tới
/topic/listings/{listingId}. - Client nhận event và refresh phần dữ liệu cần hiển thị bằng API nghiệp vụ nếu event có thể ảnh hưởng dữ liệu nhạy cảm.
Payment order flow
- Payment module xác nhận order thành công từ IPN checkout, QR bank-transfer webhook hoặc detail sync.
- Backend set
PaymentOrder.status = SUCCESS, lưupaidAt/provider transaction id và publishPaymentOrderConfirmedEvent. PaymentOrderRealtimeListenerchỉ chạy sau commit rồi pushPaymentOrderRealtimePayloadtới owner qua/user/queue/payment-orders.- Client nhận event và refresh order/wallet state bằng REST để reconcile dữ liệu bền vững.
Error flow
- STOMP handler hoặc interceptor phát sinh lỗi.
- Backend chuẩn hóa lỗi thành
WebSocketErrorResponsenếu lỗi đi qua handler hỗ trợ. - Client nhận lỗi qua
/user/queue/errorshoặc bị disconnect nếu lỗi xảy ra trongCONNECT.
Session states
DISCONNECTED: client chưa mở WebSocket hoặc vừa mất kết nối.CONNECTING: client đang gửi STOMPCONNECTtới/api/wshoặc/api/ws-sockjs.CONNECTED: STOMP session đã xác thực thành công, client có thể subscribe.SUBSCRIBED: client đã subscribe các destination cần nhận message.RECONNECTING: client mất kết nối và đang thử kết nối lại.UNAUTHORIZED:CONNECTbị từ chối do thiếu hoặc sai access token.
NotificationResponse
Dùng chung với REST notification list và WebSocket notification message:
id,userIdtype,category,prioritytitle,body,linkrefType,refId,iconactorCount,latestActorread,readAt,expiredAt,createdAt
UnreadCountResponse
{ "count": 3}ListingEventPayload
Payload listing event là tín hiệu realtime để client biết listing thay đổi. Client không nên dùng payload này để bypass quyền xem dữ liệu chi tiết.
PaymentOrderRealtimePayload
Dùng cho /user/queue/payment-orders khi payment order đổi trạng thái:
orderId,userIdeventType— hiện dùngSTATUS_CHANGEDstatus— hiện pushSUCCESSkhi order được xác nhận thanh toánamount,totalprovider,providerOrderId,providerTransactionIdpaidAt,occurredAt
Client nên coi payload này là tín hiệu realtime để refresh màn payment/wallet, không phải nguồn sự thật duy nhất.
WebSocketErrorResponse
Payload lỗi realtime dùng cho /user/queue/errors, gồm thông tin lỗi đủ để client hiển thị message thân thiện và ghi log debug khi cần.
Cấu hình runtime
| Env/config | Mục đích |
|---|---|
APP_WS_ALLOWED_ORIGINS | Origin được phép mở WebSocket |
CORS_ALLOWED_ORIGINS | Fallback allowed origins dùng chung CORS |
APP_WS_HEARTBEAT_CLIENT_MS | STOMP heartbeat từ client |
APP_WS_HEARTBEAT_SERVER_MS | STOMP heartbeat từ server |
RL_WEBSOCKET | Rate limit bucket cho inbound WebSocket |
JWT_SECRET, JWT_ISSUER | Cấu hình JWT dùng để xác thực CONNECT |
Giới hạn hiện tại
- WebSocket là kênh bổ sung cho realtime; dữ liệu nguồn sự thật vẫn nằm ở API/DB.
- Notification realtime không thay thế lịch sử notification; nếu client bỏ lỡ message, màn notification phải lấy lại dữ liệu từ API danh sách notification.
- Unread count realtime là snapshot tại thời điểm server push; client không nên tự cộng/trừ nếu đã có response mới hơn từ API.
- Payment order realtime chỉ báo trạng thái mới nhất sau commit; client cần gọi REST order/wallet API để reconcile số dư sau khi nhận event hoặc reconnect.
- Listing event trên
/topic/listings/{listingId}chỉ là tín hiệu cập nhật; client cần quyền xem listing theo API nghiệp vụ trước khi hiển thị dữ liệu nhạy cảm. - Server code publish qua facade WebSocket của module tương ứng, không inject trực tiếp
SimpMessagingTemplatevào business service khi đã có wrapper sẵn. - Simple broker đang chạy in-memory, phù hợp một backend instance.
- Khi scale nhiều instance, cần chuyển sang external STOMP relay như RabbitMQ hoặc broker tương đương.
- Hiện tại WebSocket chủ yếu phục vụ server push; chưa có nhiều flow client gửi message nghiệp vụ qua
@MessageMapping. - Client vẫn phải có REST reconcile sau reconnect, vì WebSocket không phải nguồn dữ liệu bền vững.
Câu hỏi mở
- Khi deploy multi-instance, thời điểm nào cần chuyển từ simple broker sang external STOMP relay?
- Có cần chuẩn hóa version field trong payload realtime để FE/Mobile migrate message schema an toàn hơn không?
- Có cần chính sách retry/backoff chuẩn cho FE/Mobile khi WebSocket reconnect liên tục không?
- Có cần bổ sung
@MessageMappingcho các use case client gửi message realtime, hay tiếp tục giữ server-push only?
Liên quan
- Notification system: Notification
- Rate limit system: Rate Limit
- Listing distribution: Daily Listing Distribution cho Broker theo gói
- Payment detail/IPN/webhook: Lấy chi tiết đơn hàng thanh toán
- QR bank-transfer webhook: Tạo đơn hàng nạp tiền ví V-credit (Webhook VietQR)