Documentation

Direct messaging

One-to-one messaging — REST for history, WebSocket for live delivery.

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

POST/community/messagesJWT
GET/community/messages/threadsJWT
GET/community/messages/thread/:userIdJWT
POST/community/messages/readJWT
POST/community/messages/thread/:userId/readJWT
DELETE/community/messages/:idJWT
GET/community/messages/unread-countJWT

The 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.