Zum Inhalt springen

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

  1. Hol dir ein Ticket mit POST /api/realtime/ticket, mit Token und X-CommunityId (siehe Authentifizierung und X-CommunityId). Das Ticket gilt einmal, 60 Sekunden lang und ist an Person, Community und Sitzung gebunden.
  2. Öffne wss://<api>/api/realtime/socket (data.path) und gib das Ticket als zweites Subprotokoll nach memberfy-realtime mit (data.protocols). Der Server antwortet mit memberfy-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:

RaumWer
meDie Person selbst
communityAlle in der Community
staffNur 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:

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

ThemaRaumWann
event.livecommunity, space:<id>, item:event:<id>Ein Event hat die Übertragung begonnen oder beendet
eventscommunity, space:<id>, item:event:<id>Ein Event wurde angelegt, bearbeitet oder entfernt
analytics.onlinestaff (Eigentümer und Admin)„Jetzt online“ hat sich geändert (höchstens alle 5 Sekunden)
campaigns.statsstaffEin Kampagnenversand ist fertig
menu.changedcommunityDas 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.join beim Öffnen der Seite: Der Raum wird wie bei subscribe geprüft (und abonniert, falls noch nicht geschehen), höchstens 3 Plätze pro Verbindung. Die Antwort ist presence.joined (room, editing) gefolgt von einem presence-Abbild, oder presence.denied (room), ohne Grund.
  • presence.leave beim 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.active bei 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. members enthält höchstens 5 Personen (zuerst aktive Bearbeiter, dann aktive, dann inaktive); count ist die Gesamtzahl der anderen Personen im Raum.
  • Nur Lesende: In Bereichs- und Elementräumen bekommt, wer dort nicht bearbeitet, nur memberId, name, avatarUrl und state, ohne editing, lastActiveAt und editingCount, 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 (members leer, count 0), auch das Team. Nur wer dasselbe Element bearbeitet, sieht die anderen Bearbeiter weiter; item.saved und 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.
CodeBedeutungWas tun
4004Sitzung beendetNicht mit derselben Sitzung neu verbinden
4009Ersetzt: Die Person hat mehr als 10 Verbindungen geöffnet, die älteste wurde geschlossenNichts, wenn es der alte Tab ist
4029Server vollSpäter neu verbinden und solange die Routen nutzen
1008Zu viele FramesLangsamer senden
1001Server startet neuNeu verbinden

Verwandte Artikel