Skip to content

Realtime (WebSocket)

The platform WebSocket - the single-use ticket, the memberfy-realtime subprotocol, automatic and requested rooms, invalidate notices, limits and close codes.

The app gets changes right away through a WebSocket: the bell, the chat, "Online now", who is on each screen, the live event banner, campaign numbers. Any member of the community, in any role, opens the socket, with the chat on or off.

Opening the connection

  1. Ask for a ticket with POST /api/realtime/ticket, with the token and X-CommunityId (see Authentication and X-CommunityId). The ticket is single-use, valid for 60 seconds and tied to the person, the community and the session.
  2. Open wss://<api>/api/realtime/socket (data.path) passing the ticket as the second subprotocol, after memberfy-realtime (data.protocols). The server answers with memberfy-realtime.
const { data } = await api.post('/api/realtime/ticket'); // with Authorization and X-CommunityId
const ws = new WebSocket(`wss://api.memberfy.net${data.path}`, data.protocols);

The ticket never goes in the URL (in the query it is ignored). Without a valid ticket, the connection is refused before it becomes a WebSocket (401). Switching community closes the socket: ask for another ticket. Without the realtime service, the ticket returns 503, and the app keeps fetching through the routes.

POST /api/chat/ticket and /api/chat/socket keep working as aliases of these for one deploy and then go away: use the new ones.

Rooms

Every connection is already in three rooms:

RoomWho
meThe person themself
communityEveryone in the community
staffThe team only (owner, admin, moderator and finance)

The others are requested by the client, with the same rule as the matching GET, always in this community:

RoomWho gets in
space:<id>Whoever can open the space
item:event:<id>Whoever can open the event
item:post:<id>Whoever can open the post
{ "type": "subscribe", "rooms": ["space:5b0e7c1a-…", "item:event:9d2f…"] }
{ "type": "unsubscribe", "rooms": ["space:5b0e7c1a-…"] }

There are also rooms for the editors, only for the roles that can save there: item:course:<id>, item:form:<id> (the team and whoever manages the form; a member when it is published and they open the space), item:campaign:<id>, item:email:<id> (library e-mail), item:workflow:<id>, item:survey:<id> (owner and admin), item:certificateTemplate:<id> (whoever issues certificates in the space), item:emailOverride:<emailKey> (default e-mail, owner and admin). And the area rooms, one per section screen: area:home (everyone), area:members, area:campaigns, area:emails, area:workflows (team), area:moderation (owner, admin and moderator), area:analytics, area:dashboard, area:settings (owner and admin) and area:finance (owner, admin and finance).

At most 20 rooms per connection (the automatic ones don't count). The answer is subscribed with the rooms taken and one subscribe.denied per refused room, with no reason. When the person's access changes (left, new role, group, space access), the server re-checks the rooms, sends subscribe.denied for the ones lost and then access.changed.

Change notices (invalidate)

{ "type": "invalidate", "room": "community", "topic": "event.live", "ids": ["9d2f…"], "at": "2026-10-23T14:05:00.000Z" }

The notice carries only ids and the topic, never a title, text, amount or name: when you get it, redo the GET of what is on screen through the usual routes.

TopicRoomWhen
event.livecommunity, space:<id>, item:event:<id>An event started or ended its stream
eventscommunity, space:<id>, item:event:<id>An event was created, edited or removed
analytics.onlinestaff (owner and admin)"Online now" changed (at most once every 5 seconds)
campaigns.statsstaffA campaign send finished
menu.changedcommunityThe menu changed: a section or space created, edited (name, icon, address, section, access, visibility), removed or reordered. No ids and no menu data: each person redoes GET /api/sections with their own permissions. Changes in a row become a single notice, within 1 second

Event notices only reach whoever sees that space; a private event or one under moderation, only the team.

Who is here

Area presence (the faces at the top of each screen, see Who is here and team editing) uses the same rooms.

{ "type": "presence.join", "room": "area:campaigns", "editing": false }
{ "type": "presence.leave", "room": "area:campaigns" }
{ "type": "presence.active" }
  • presence.join when the screen opens: the room is checked as in subscribe (and subscribed if it was not yet), at most 3 places per connection. The answer is presence.joined (room, editing) followed by a presence snapshot, or presence.denied (room), with no reason.
  • presence.leave when leaving. There is no heartbeat: the server ping keeps the place, which goes away 45 seconds after the last answer; closing the connection removes it at once.
  • presence.active on interaction (key, click, focus), at most every 10 seconds. Joining counts as activity.

On each change (at most 1 per second per room), the room gets a snapshot built for each connection:

{
  "type": "presence", "room": "area:campaigns", "area": "area:campaigns",
  "members": [{ "memberId": "…", "name": "Maria", "avatarUrl": "…", "editing": false, "state": "active", "lastActiveAt": "…" }],
  "count": 3, "editingCount": 1, "at": "2026-10-28T14:05:00.000Z"
}
  • You are never in your own list. members has at most 5 people (active editors first, then active, then idle); count is the total of the other people in the room.
  • Readers: in space and item rooms, someone not editing there gets only memberId, name, avatarUrl and state, without editing, lastActiveAt or editingCount, to protect the editors' privacy. People editing the same item and the team area rooms get the full snapshot.
  • state: "idle": connected, no activity for 30 seconds or more. Still listed.
  • Privacy: someone who turned off Available to chat does not appear or count for others, but sees who is there. A chat block hides the two people from each other.
  • Community chat off: area rooms and readers get nobody (members empty, count 0), team included. Only people editing the same item still see the other editors; item.saved and the version 409 do not change.

Editing and item.saved

In an editor (event, campaign, library e-mail, workflow, course, form, survey, certificate template, default e-mail and the settings, in area:settings), join with "editing": true. It only counts for whoever can save there (the team and, on a form, whoever manages it). An editor appears with editing: true to the other editors in the room, even with the chat status off; someone just reading the page is not told. editingCount counts only active editors.

On each save, the room gets, for the team and for whoever is editing:

{ "type": "item.saved", "topic": "item.saved", "room": "item:campaign:<id>", "itemType": "campaign", "itemId": "<id>", "by": { "memberId": "…", "name": "Maria" }, "updatedAt": "<new version>", "at": "…" }

Compare updatedAt with the version you hold to ignore your own save. itemType: event, campaign, email, workflow, course (including modules and lessons), form, survey, certificateTemplate, emailOverride (itemId = emailKey) and, in the area:settings room (itemId = the community), settings, chatSettings, notificationSettings, messagingSettings (SMS + WhatsApp), creditSettings, businessInformation, payoutSettings.

Saves accept the version read and answer 409 when another came first: see Version conflict.

Other events

From the server, as JSON { type, data }: ready (with profileId, communityId, chatAvailable, chatStatus (the chosen chat status), chatEnabled, rooms, maxRooms and pingIntervalMs), notification.new, notification.read and notification.removed (the bell, in every community), chat.settings, session.revoked and pong. Chat events only arrive with the chat on; with the chat off, the socket stays up and only those stop.

From the client: subscribe, unsubscribe, presence.join, presence.leave, presence.active, presence.idle ({ "type": "presence.idle", "idle": true } when the tab has been idle for 10 minutes, false when it comes back: the person shows as Away while all their tabs are idle), typing (only with the chat on) and ping.

Limits and close codes

  • Frames up to 4 KB, no binary, at most 5 per second.
  • The server sends a ping every 25 seconds.
  • To reconnect, wait a random, growing delay and ask for a new ticket.
CodeWhat it meansWhat to do
4004Session endedDon't reconnect with the same session
4009Replaced: the person opened more than 10 connections, and the oldest closedNothing, if it is the old tab
4029Server fullConnect again later and use the routes meanwhile
1008Too many framesSlow down
1001Server restartingReconnect