Direct messaging is Community's one-to-one channel between two customers. Messages are persisted via REST and pushed in real time over the CommunityChatGateway WebSocket namespace. The DM API is intentionally separate from group chat — DM threads have implicit two-person membership and don't need member-management endpoints.
REST endpoints
/community/messagesJWT/community/messages/threadsJWT/community/messages/thread/:userIdJWT/community/messages/readJWT/community/messages/thread/:userId/readJWT/community/messages/:idJWT/community/messages/unread-countJWTThe same routes are mirrored under /client/community/messages/* for customer JWTs.
Sending a message via REST
await fetch('/community/messages', {
method: 'POST',
headers: {
orgid: 'my-org',
Authorization: `Bearer ${jwt}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: '[email protected]',
content: 'Hey, can you take a look at the proposal?',
contentType: 'text', // 'text' | 'image' | 'file' | 'audio'
attachments: [], // [{ url, type, name, size }]
replyTo: 'optional-message-id',
}),
});
The service derives from from the JWT, persists the message, fans out a newMessage socket event to the recipient if they're connected, and writes a notification record.
Reading thread history
GET /community/messages/threads returns the caller's thread list — one entry per peer email — with the last message preview and unread count. GET /community/messages/thread/:userId returns the full message history with that peer.
const threads = await fetch('/community/messages/threads', {
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` },
}).then(r => r.json());
// [{ peer: 'bob@...', peerInfo: {...}, lastMessage: {...}, unreadCount: 3 }, ...]
Read receipts
// Mark all messages in a thread as read
await fetch(`/community/messages/thread/${peerEmail}/read`, {
method: 'POST',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` },
});
// Mark specific message ids as read
await fetch('/community/messages/read', {
method: 'POST',
headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: ['msg-1', 'msg-2'] }),
});
WebSocket gateway
The CommunityChatGateway lives at the /community-chat namespace. The customer JWT is sent in the Socket.IO auth payload, not as an Authorization header.
import { io } from 'socket.io-client';
const socket = io('https://appengine.appmint.io/community-chat', {
transports: ['websocket'],
auth: {
token: customerJwt,
orgId: 'my-org',
},
});
socket.on('connect', () => console.log('connected'));
socket.on('newMessage', (msg) => {
// msg: { id, from, to, content, contentType, createdAt, ... }
});
socket.on('messageRead', ({ messageId, by }) => { /* update UI */ });
socket.on('typing', ({ from }) => { /* show indicator */ });
socket.on('userOnline', ({ email }) => { /* presence */ });
socket.on('userOffline', ({ email }) => { /* presence */ });
Socket events you can emit
// Send a message via WS (also persists to DB)
socket.emit('sendMessage', {
to: '[email protected]',
content: 'Quick one',
contentType: 'text',
});
// Typing indicator
socket.emit('typing', { to: '[email protected]' });
// Mark as read
socket.emit('markRead', { messageId: '...' });
// Online users in this org (returns via callback)
socket.emit('getOnlineUsers', null, (users) => { /* ... */ });
On connect, the gateway joins the customer to two rooms automatically: their own email (for direct messages) and org:<orgId> (for org-wide broadcasts). Group rooms are joined on demand — see Group chat.
Unread counts
GET /community/messages/unread-count returns { count: number } across all threads — call it on app boot to drive the badge in your nav, then keep it in sync with the newMessage and messageRead socket events.
DMs are scoped per org — a customer in org A cannot DM a customer in org B even if their emails match. The orgId from the socket handshake (or REST header) is enforced on every message write.