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
- Ask for a ticket with
POST /api/realtime/ticket, with the token andX-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. - Open
wss://<api>/api/realtime/socket(data.path) passing the ticket as the second subprotocol, aftermemberfy-realtime(data.protocols). The server answers withmemberfy-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:
| Room | Who |
|---|---|
me | The person themself |
community | Everyone in the community |
staff | The 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:
| Room | Who 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.
| Topic | Room | When |
|---|---|---|
event.live | community, space:<id>, item:event:<id> | An event started or ended its stream |
events | community, space:<id>, item:event:<id> | An event was created, edited or removed |
analytics.online | staff (owner and admin) | "Online now" changed (at most once every 5 seconds) |
campaigns.stats | staff | A campaign send finished |
menu.changed | community | The 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.joinwhen the screen opens: the room is checked as insubscribe(and subscribed if it was not yet), at most 3 places per connection. The answer ispresence.joined(room,editing) followed by apresencesnapshot, orpresence.denied(room), with no reason.presence.leavewhen 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.activeon 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.
membershas at most 5 people (active editors first, then active, then idle);countis the total of the other people in the room. - Readers: in space and item rooms, someone not editing there gets only
memberId,name,avatarUrlandstate, withoutediting,lastActiveAtoreditingCount, 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 (
membersempty,count0), team included. Only people editing the same item still see the other editors;item.savedand the version409do 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
pingevery 25 seconds. - To reconnect, wait a random, growing delay and ask for a new ticket.
| Code | What it means | What to do |
|---|---|---|
4004 | Session ended | Don't reconnect with the same session |
4009 | Replaced: the person opened more than 10 connections, and the oldest closed | Nothing, if it is the old tab |
4029 | Server full | Connect again later and use the routes meanwhile |
1008 | Too many frames | Slow down |
1001 | Server restarting | Reconnect |