Files
nick-doc/03 - API Reference/Socket Events.md
2026-05-23 20:35:34 +03:30

8.4 KiB

title, tags
title tags
Socket Events
api
socket
realtime
reference

Socket Events

The backend runs a Socket.IO server on the same HTTP port as the REST API. It is initialised in backend/src/app.ts and exposed globally as global.io. Helper functions for emitting events from services live in backend/src/infrastructure/socket/socketService.ts:

emitGlobalEvent(event, data);          // io.emit
emitToRoom(room, event, data);         // io.to(room).emit
setSocketServer(server);
getSocketServer();

Connection

import { io } from "socket.io-client";

const socket = io(process.env.NEXT_PUBLIC_API_URL!, {
  withCredentials: true,
  transports: ["websocket"],
});

The CORS policy mirrors the REST API: only FRONTEND_URL is allowed, and credentials are enabled. There is no token-based handshake — the server identifies users by the join-* events the client emits after connecting. (A future improvement would authenticate via the JWT on handshake; for now, ownership is checked at the REST layer when a mutation triggers an event.)

The server logs 🔌 User connected: <socketId> on connect and 🔌 User disconnected: <socketId> on disconnect. There is no broadcast user-offline on disconnect — see "Online status" below.

Rooms

Rooms are the targeting mechanism. The client joins/leaves rooms with these events:

Client → Server Room joined
join-user-room (userId) user-<userId>
join-request-room (requestId) request-<requestId>
leave-request-room (requestId) leaves request-<requestId>
join-seller-room (sellerId) seller-<sellerId> + sellers (global)
leave-seller-room (sellerId) leaves both
join-buyer-room (buyerId) buyer-<buyerId> + buyers (global)
leave-buyer-room (buyerId) leaves both
join-chat-room (chatId) chat-<chatId>
leave-chat-room (chatId) leaves chat-<chatId>
user-online (userId) joins user-<userId>, broadcasts user-status-change
typing-start ({ chatId, userId, userName }) broadcasts user-typing to chat-<chatId>
typing-stop ({ chatId, userId }) broadcasts user-typing (isTyping=false)

Joining a room is unauthenticated — clients are expected to only join their own rooms. Sensitive data is filtered at the REST layer that emits the event.

Server → Client events

Grouped by the service that emits them.

Marketplace

Event Room Payload Source
new-purchase-request sellers PurchaseRequest document marketplaceController.createPurchaseRequest
new-offer buyer-<buyerId> { requestId, offer, sellerId } marketplaceController.createSellerOffer
seller-offer-update seller-<sellerId> (and global on payment confirm) { sellerId, requestId, eventType: "payment-completed" | "offer-rejected" | "offer-accepted", data: { offerId, isSelected, paymentId?, transactionHash?, reason? } } marketplaceController, shkeeperRoutes, shkeeperWebhook, SellerOfferService
purchase-request-update request-<requestId> { type, requestId, status?, paymentId?, txHash?, provider? } marketplaceController, PurchaseRequestService, shkeeperRoutes, paymentCoordinator
request-cancelled user-<buyerId>, user-<sellerId> { requestId, reason } PurchaseRequestService
transaction-completed user-<buyerId>, user-<sellerId> { requestId, paymentId, amount, currency } marketplaceController
delivery-code-generated request-<requestId> { requestId, deliveryCode } (only the seller UI uses this) DeliveryService
delivery-update request-<requestId> { requestId, status, carrier?, trackingNumber? } DeliveryService
delivery-confirmed request-<requestId> { requestId } DeliveryService
buyer-confirmed-delivery user-<sellerId> { requestId, buyerId } DeliveryService
template-checkout-payment-confirmed global + template-checkout-<id> { checkoutId, requestIds, paymentId } templateCheckoutWebhook, paymentCoordinator
template-checkout-payment-pending global { checkoutId } templateCheckoutWebhook
template-checkout-payment-failed global { checkoutId, reason } templateCheckoutWebhook

Payment

Event Room Payload Source
payment-created global { paymentId, provider, requestId, buyerId, sellerId, amount, currency } shkeeperService, decentralizedPaymentService
payment-received user-<sellerId> { purchaseRequestId, amount, currency, buyerId } paymentRoutes, marketplace/routes
payment-update global + room-specific { paymentId, status, escrowState?, txHash? } paymentCoordinator
payout-created global { payoutId, taskId, sellerId, amount, currency } shkeeperPayoutService
payout-completed global, user-<sellerId> { payoutId, taskId, txHash } shkeeperPayoutService, decentralizedPaymentService
payout-updated global { payoutId, status } shkeeperPayoutService

Chat

Event Room Payload
new-message chat-<chatId> { chatId, message: { _id, content, senderId, createdAt, attachments? }, senderId }
messages-read chat-<chatId> { chatId, userId, upToMessageId, modifiedCount }
message-edited chat-<chatId> { chatId, messageId, content, editedAt }
message-deleted chat-<chatId> { chatId, messageId, deletedAt }
participants-added chat-<chatId> { chatId, addedUserIds }
participant-removed chat-<chatId> { chatId, removedUserId }
user-typing chat-<chatId> { userId, userName?, isTyping }
user-status-change broadcast { userId, status: "online", lastSeen }

Sources: ChatService.ts, chatController.ts, and app.ts socket handlers.

Notification

Event Room Payload
new-notification user-<userId> { notification: { _id, type, title, body, data, createdAt } }
unread-count-update user-<userId> { unreadCount }

Source: NotificationService.ts.

Points

Event Room Payload
level-up user-<userId> { oldLevel, newLevel, lifetimePoints, perks }
referral-reward user-<referrerId> { referredUserId, points, transactionId }
referral-signup user-<referrerId> { referredUserId, name, joinedAt }

Sources: PointsService.ts, authController.ts.

Online status

A client emits user-online after connecting; the server broadcasts user-status-change (status "online"). There is currently no matching offline emit on disconnect because the server does not track which userId belongs to which socketId. To implement presence, store the mapping on connect and emit user-status-change (status "offline") in the disconnect handler.

Reconnection

Socket.IO defaults are used: exponential backoff starting at 1s, capped at 5s, with jitter. The recommended client policy is:

  1. On reconnect, re-emit every join-* for the rooms the user cares about.
  2. Re-fetch any state that may have moved while disconnected (GET /api/notifications/unread-count, the active purchase request, chat history with ?before=<lastSeen>).
  3. Treat the first 5 seconds after reconnect as "catching up" in the UI.

The server does not buffer missed events; if delivery guarantees matter, fall back to REST.

Server-side helpers (for service authors)

import { emitGlobalEvent, emitToRoom } from "@/infrastructure/socket/socketService";

emitGlobalEvent("new-purchase-request", request);
emitToRoom(`request-${requestId}`, "purchase-request-update", { type: "status_changed", status });

Use emitToRoom whenever you can — it limits the broadcast surface area. Reserve emitGlobalEvent for events that genuinely need every connected client (rare).