Echtzeit (WebSocket)
Der WebSocket der Plattform - das Einmal-Ticket, das Subprotokoll memberfy-realtime, automatische und angefragte Räume, invalidate-Hinweise, Limits und Close-Codes.
Die App bekommt Änderungen sofort über einen WebSocket: die Glocke, den Chat, „Jetzt online“, wer auf welcher Seite ist, das Live-Banner von Events, die Kampagnenzahlen. Jedes Mitglied der Community, in jeder Rolle, öffnet den Socket, mit Chat an oder aus.
Die Verbindung öffnen
- Hol dir ein Ticket mit
POST /api/realtime/ticket, mit Token undX-CommunityId(siehe Authentifizierung und X-CommunityId). Das Ticket gilt einmal, 60 Sekunden lang und ist an Person, Community und Sitzung gebunden. - Öffne
wss://<api>/api/realtime/socket(data.path) und gib das Ticket als zweites Subprotokoll nachmemberfy-realtimemit (data.protocols). Der Server antwortet mitmemberfy-realtime.
const { data } = await api.post('/api/realtime/ticket'); // mit Authorization und X-CommunityId
const ws = new WebSocket(`wss://api.memberfy.net${data.path}`, data.protocols);
Das Ticket kommt nie in die URL (in der Query wird es ignoriert). Ohne gültiges Ticket wird die Verbindung abgelehnt, bevor sie zum WebSocket wird (401). Ein Community-Wechsel schließt den Socket: hol dir ein neues Ticket. Ohne den Echtzeit-Dienst liefert das Ticket 503, und die App lädt weiter über die Routen.
POST /api/chat/ticket und /api/chat/socket funktionieren noch ein Deploy lang als Alias und entfallen dann: nutze die neuen.
Räume
Jede Verbindung ist schon in drei Räumen:
| Raum | Wer |
|---|---|
me | Die Person selbst |
community | Alle in der Community |
staff | Nur das Team (Eigentümer, Admin, Moderator und Finanzen) |
Die anderen fragt der Client an, mit derselben Regel wie beim passenden GET, immer in dieser Community:
| Raum | Wer hineinkommt |
|---|---|
space:<id> | Wer den Bereich öffnen darf |
item:event:<id> | Wer das Event öffnen darf |
item:post:<id> | Wer den Beitrag öffnen darf |
{ "type": "subscribe", "rooms": ["space:5b0e7c1a-…", "item:event:9d2f…"] }
{ "type": "unsubscribe", "rooms": ["space:5b0e7c1a-…"] }
Außerdem gibt es Räume für die Editoren, nur für die Rollen, die dort speichern dürfen: item:course:<id>, item:form:<id> (das Team und wer das Formular verwaltet; ein Mitglied, wenn es veröffentlicht ist und es den Bereich öffnet), item:campaign:<id>, item:email:<id> (E-Mail der Bibliothek), item:workflow:<id>, item:survey:<id> (Inhaber und Admin), item:certificateTemplate:<id> (wer im Bereich Zertifikate ausstellt), item:emailOverride:<emailKey> (Standard-E-Mail, Inhaber und Admin). Und die Bereichsräume, einer pro Abschnittsseite: area:home (alle), area:members, area:campaigns, area:emails, area:workflows (Team), area:moderation (Inhaber, Admin und Moderator), area:analytics, area:dashboard, area:settings (Inhaber und Admin) und area:finance (Inhaber, Admin und Finanzen).
Höchstens 20 Räume pro Verbindung (die automatischen zählen nicht). Die Antwort ist subscribed mit den angenommenen Räumen und ein subscribe.denied je abgelehntem Raum, ohne Grund. Ändert sich der Zugang der Person (ausgetreten, neue Rolle, Gruppe, Bereichszugang), prüft der Server die Räume neu, schickt subscribe.denied für die verlorenen und danach access.changed.
Änderungshinweise (invalidate)
{ "type": "invalidate", "room": "community", "topic": "event.live", "ids": ["9d2f…"], "at": "2026-10-23T14:05:00.000Z" }
Der Hinweis enthält nur IDs und das Thema, nie Titel, Text, Betrag oder Namen: Lade danach das, was auf dem Bildschirm ist, mit dem üblichen GET neu.
| Thema | Raum | Wann |
|---|---|---|
event.live | community, space:<id>, item:event:<id> | Ein Event hat die Übertragung begonnen oder beendet |
events | community, space:<id>, item:event:<id> | Ein Event wurde angelegt, bearbeitet oder entfernt |
analytics.online | staff (Eigentümer und Admin) | „Jetzt online“ hat sich geändert (höchstens alle 5 Sekunden) |
campaigns.stats | staff | Ein Kampagnenversand ist fertig |
menu.changed | community | Das Menü hat sich geändert: Abschnitt oder Bereich angelegt, bearbeitet (Name, Symbol, Adresse, Abschnitt, Zugriff, Sichtbarkeit), entfernt oder neu geordnet. Ohne ids und ohne Daten des Menüs: Jede Person ruft GET /api/sections mit den eigenen Berechtigungen neu ab. Änderungen kurz hintereinander werden innerhalb von 1 Sekunde zu einem einzigen Hinweis |
Event-Hinweise erreichen nur, wer den Bereich sieht; ein privates Event oder eines in Moderation nur das Team.
Wer ist hier
Die Anwesenheit (die Gesichter oben auf jeder Seite, siehe Wer ist hier und gemeinsames Bearbeiten) nutzt dieselben Räume.
{ "type": "presence.join", "room": "area:campaigns", "editing": false }
{ "type": "presence.leave", "room": "area:campaigns" }
{ "type": "presence.active" }
presence.joinbeim Öffnen der Seite: Der Raum wird wie beisubscribegeprüft (und abonniert, falls noch nicht geschehen), höchstens 3 Plätze pro Verbindung. Die Antwort istpresence.joined(room,editing) gefolgt von einempresence-Abbild, oderpresence.denied(room), ohne Grund.presence.leavebeim Verlassen. Es gibt keinen Heartbeat: Der Server-Ping hält den Platz, der 45 Sekunden nach der letzten Antwort verschwindet; das Schließen der Verbindung entfernt ihn sofort.presence.activebei Interaktion (Taste, Klick, Fokus), höchstens alle 10 Sekunden. Das Betreten zählt als Aktivität.
Bei jeder Änderung (höchstens 1 pro Sekunde und Raum) bekommt der Raum ein Abbild, das für jede Verbindung gebaut wird:
{
"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"
}
- Du bist nie in deiner eigenen Liste.
membersenthält höchstens 5 Personen (zuerst aktive Bearbeiter, dann aktive, dann inaktive);countist die Gesamtzahl der anderen Personen im Raum. - Nur Lesende: In Bereichs- und Elementräumen bekommt, wer dort nicht bearbeitet, nur
memberId,name,avatarUrlundstate, ohneediting,lastActiveAtundeditingCount, zum Schutz der Bearbeiter. Wer dasselbe Element bearbeitet, und die Bereichsräume des Teams bekommen das vollständige Abbild. state: "idle": verbunden, seit 30 Sekunden oder mehr ohne Aktivität. Bleibt in der Liste.- Datenschutz: Wer Für Chats verfügbar ausgeschaltet hat, erscheint und zählt bei den anderen nicht, sieht aber, wer da ist. Eine Chat-Blockierung verbirgt die beiden Personen voreinander.
- Community-Chat aus: Bereichsräume und nur Lesende bekommen niemanden (
membersleer,count0), auch das Team. Nur wer dasselbe Element bearbeitet, sieht die anderen Bearbeiter weiter;item.savedund der Versions-409ändern sich nicht.
Bearbeiten und item.saved
In einem Editor (Event, Kampagne, E-Mail der Bibliothek, Workflow, Kurs, Formular, Umfrage, Zertifikatsvorlage, Standard-E-Mail und die Einstellungen in area:settings) mit "editing": true beitreten. Das gilt nur für alle, die dort speichern dürfen (das Team und beim Formular, wer es verwaltet). Wer bearbeitet, erscheint mit editing: true bei den anderen Bearbeitern im Raum, auch mit ausgeschaltetem Chat-Status; wer die Seite nur liest, erfährt davon nichts. editingCount zählt nur aktive Bearbeiter.
Bei jedem Speichern bekommt der Raum, für das Team und für alle, die gerade bearbeiten:
{ "type": "item.saved", "topic": "item.saved", "room": "item:campaign:<id>", "itemType": "campaign", "itemId": "<id>", "by": { "memberId": "…", "name": "Maria" }, "updatedAt": "<neue Version>", "at": "…" }
Vergleiche updatedAt mit deiner Version, um dein eigenes Speichern zu ignorieren. itemType: event, campaign, email, workflow, course (auch Module und Lektionen), form, survey, certificateTemplate, emailOverride (itemId = emailKey) und im Raum area:settings (itemId = die Community) settings, chatSettings, notificationSettings, messagingSettings (SMS + WhatsApp), creditSettings, businessInformation, payoutSettings.
Speichervorgänge nehmen die gelesene Version an und antworten mit 409, wenn ein anderer zuerst kam: siehe Versionskonflikt.
Weitere Ereignisse
Vom Server, als JSON { type, data }: ready (mit profileId, communityId, chatAvailable, chatStatus (der gewählte Chat-Status), chatEnabled, rooms, maxRooms und pingIntervalMs), notification.new, notification.read und notification.removed (die Glocke, in jeder Community), chat.settings, session.revoked und pong. Chat-Ereignisse kommen nur bei eingeschaltetem Chat; ist der Chat aus, bleibt der Socket bestehen, und nur sie fallen weg.
Vom Client: subscribe, unsubscribe, presence.join, presence.leave, presence.active, presence.idle ({ "type": "presence.idle", "idle": true }, wenn der Tab 10 Minuten inaktiv ist, false, wenn er zurückkommt: Die Person erscheint als Abwesend, solange alle ihre Tabs inaktiv sind), typing (nur mit Chat an) und ping.
Limits und Close-Codes
- Frames bis 4 KB, kein Binär, höchstens 5 pro Sekunde.
- Der Server sendet alle 25 Sekunden ein
ping. - Zum Neuverbinden warte eine zufällige, wachsende Zeit und hol dir ein neues Ticket.
| Code | Bedeutung | Was tun |
|---|---|---|
4004 | Sitzung beendet | Nicht mit derselben Sitzung neu verbinden |
4009 | Ersetzt: Die Person hat mehr als 10 Verbindungen geöffnet, die älteste wurde geschlossen | Nichts, wenn es der alte Tab ist |
4029 | Server voll | Später neu verbinden und solange die Routen nutzen |
1008 | Zu viele Frames | Langsamer senden |
1001 | Server startet neu | Neu verbinden |