Tiempo real (WebSocket)
El WebSocket de la plataforma - el ticket de un solo uso, el subprotocolo memberfy-realtime, las salas automáticas y las pedidas, los avisos invalidate, los límites y los códigos de cierre.
La app recibe los cambios al momento por un WebSocket: la campana, el chat, el "En línea ahora", quién está en cada pantalla, el banner del evento en directo, los números de las campañas. Cualquier miembro de la comunidad, de cualquier rol, abre el socket, con el chat activado o desactivado.
Abrir la conexión
- Pide un ticket con
POST /api/realtime/ticket, con el token y elX-CommunityId(ver Autenticación y X-CommunityId). El ticket es de un solo uso, vale 60 segundos y queda ligado a la persona, a la comunidad y a la sesión. - Abre
wss://<api>/api/realtime/socket(eldata.path) pasando el ticket como segundo subprotocolo, después dememberfy-realtime(eldata.protocols). El servidor responde conmemberfy-realtime.
const { data } = await api.post('/api/realtime/ticket'); // con Authorization y X-CommunityId
const ws = new WebSocket(`wss://api.memberfy.net${data.path}`, data.protocols);
El ticket nunca va en la URL (en la query, se ignora). Sin un ticket válido, la conexión se rechaza antes de convertirse en WebSocket (401). Cambiar de comunidad cierra el socket: pide otro ticket. Sin el servicio de tiempo real, el ticket devuelve 503, y la app sigue consultando por las rutas.
POST /api/chat/ticket y /api/chat/socket siguen funcionando como alias de estos durante un despliegue y después desaparecen: usa los nuevos.
Salas
Toda conexión ya entra en tres salas:
| Sala | Quién |
|---|---|
me | La propia persona |
community | Todos en la comunidad |
staff | Solo el equipo (propietario, administrador, moderador y finanzas) |
Las demás las pide el cliente, con la misma regla del GET correspondiente, siempre en esta comunidad:
| Sala | Quién entra |
|---|---|
space:<id> | Quien puede abrir el espacio |
item:event:<id> | Quien puede abrir el evento |
item:post:<id> | Quien puede abrir la publicación |
{ "type": "subscribe", "rooms": ["space:5b0e7c1a-…", "item:event:9d2f…"] }
{ "type": "unsubscribe", "rooms": ["space:5b0e7c1a-…"] }
También hay salas para los editores, solo para los roles que pueden guardar ahí: item:course:<id>, item:form:<id> (el equipo y quien gestiona el formulario; un miembro cuando está publicado y abre el espacio), item:campaign:<id>, item:email:<id> (e-mail de la biblioteca), item:workflow:<id>, item:survey:<id> (propietario y administrador), item:certificateTemplate:<id> (quien emite certificados en el espacio), item:emailOverride:<emailKey> (e-mail predeterminado, propietario y administrador). Y las salas de área, una por pantalla de sección: area:home (todos), area:members, area:campaigns, area:emails, area:workflows (equipo), area:moderation (propietario, administrador y moderador), area:analytics, area:dashboard, area:settings (propietario y administrador) y area:finance (propietario, administrador y finanzas).
Como máximo 20 salas por conexión (las automáticas no cuentan). La respuesta es subscribed con las salas aceptadas y un subscribe.denied por sala rechazada, sin motivo. Cuando el acceso de la persona cambia (salió, rol nuevo, grupo, acceso al espacio), el servidor vuelve a evaluar las salas, envía subscribe.denied de las que perdió y después access.changed.
Avisos de cambio (invalidate)
{ "type": "invalidate", "room": "community", "topic": "event.live", "ids": ["9d2f…"], "at": "2026-10-23T14:05:00.000Z" }
El aviso trae solo los ids y el tema, nunca título, texto, importe ni nombre: al recibirlo, repite el GET de lo que está en pantalla por las rutas de siempre.
| Tema | Sala | Cuándo |
|---|---|---|
event.live | community, space:<id>, item:event:<id> | Un evento empezó o terminó la transmisión |
events | community, space:<id>, item:event:<id> | Un evento se creó, se editó o se eliminó |
analytics.online | staff (propietario y administrador) | El "En línea ahora" cambió (como máximo 1 vez cada 5 segundos) |
campaigns.stats | staff | Terminó un envío de campaña |
menu.changed | community | El menú cambió: sección o espacio creado, editado (nombre, icono, dirección, sección, acceso, visibilidad), eliminado o reordenado. Sin ids y sin ningún dato del menú: cada persona vuelve a hacer el GET /api/sections con sus propios permisos. Los cambios seguidos se convierten en un solo aviso, en hasta 1 segundo |
Los avisos de evento solo llegan a quien ve ese espacio; un evento privado o en moderación, solo al equipo.
Quién está aquí
La presencia de área (las caras en la parte superior de cada pantalla, ver Quién está aquí y edición en equipo) usa las mismas salas.
{ "type": "presence.join", "room": "area:campaigns", "editing": false }
{ "type": "presence.leave", "room": "area:campaigns" }
{ "type": "presence.active" }
presence.joinal abrir la pantalla: la sala se evalúa como ensubscribe(y se suscribe si aún no lo estaba), como máximo 3 lugares por conexión. La respuesta espresence.joined(room,editing) seguido de una instantáneapresence, opresence.denied(room), sin motivo.presence.leaveal salir. No hay heartbeat: el ping del servidor mantiene el lugar, que desaparece 45 segundos después de la última respuesta; cerrar la conexión lo quita al instante.presence.activecuando hay interacción (tecla, clic, foco), como máximo cada 10 segundos. Entrar cuenta como actividad.
En cada cambio (como máximo 1 por segundo por sala), la sala recibe una instantánea montada para cada conexión:
{
"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"
}
- Tú nunca estás en tu propia lista.
memberstrae como máximo 5 personas (editando y activas primero, luego las activas, luego las inactivas);countes el total de las otras personas en la sala. - Quien solo lee: en las salas de espacio y de elemento, quien no está editando ahí recibe solo
memberId,name,avatarUrlystate, sinediting,lastActiveAtnieditingCount, para preservar a quien edita. Quien edita el mismo elemento y las salas de área del equipo reciben la instantánea completa. state: "idle": conectada, sin actividad desde hace 30 segundos o más. Sigue en la lista.- Privacidad: quien desactivó Disponible para conversar no aparece ni cuenta para los demás, pero ve quién está. Un bloqueo en el chat oculta a las dos personas entre sí.
- Chat de la comunidad desactivado: las salas de área y quien solo lee no reciben a nadie (
membersvacío,count0), incluido el equipo. Solo quien edita el mismo elemento sigue viendo a los otros editores;item.savedy el409de versión no cambian.
Edición e item.saved
En un editor (evento, campaña, e-mail de la biblioteca, workflow, curso, formulario, encuesta, plantilla de certificado, e-mail predeterminado y la configuración, en area:settings), entra con "editing": true. Solo vale para quien puede guardar ahí (el equipo y, en el formulario, quien lo gestiona). Quien edita aparece con editing: true para los otros editores de la sala, incluso con el estado del chat desactivado; quien solo lee la página no se entera. editingCount cuenta solo a quien edita activo.
En cada guardado, la sala recibe, para el equipo y para quien está editando:
{ "type": "item.saved", "topic": "item.saved", "room": "item:campaign:<id>", "itemType": "campaign", "itemId": "<id>", "by": { "memberId": "…", "name": "Maria" }, "updatedAt": "<nueva versión>", "at": "…" }
Compara updatedAt con la versión que tienes para ignorar tu propio guardado. itemType: event, campaign, email, workflow, course (incluidos módulos y lecciones), form, survey, certificateTemplate, emailOverride (itemId = emailKey) y, en la sala area:settings (itemId = la comunidad), settings, chatSettings, notificationSettings, messagingSettings (SMS + WhatsApp), creditSettings, businessInformation, payoutSettings.
Los guardados aceptan la versión leída y responden 409 cuando otro llegó antes: ver Conflicto de versión.
Otros eventos
Del servidor, en JSON { type, data }: ready (con profileId, communityId, chatAvailable, chatStatus (el estado elegido en el chat), chatEnabled, rooms, maxRooms y pingIntervalMs), notification.new, notification.read y notification.removed (la campana, en toda comunidad), chat.settings, session.revoked y pong. Los eventos del chat solo llegan con el chat activado; con el chat desactivado, el socket sigue en pie y solo esos se detienen.
Del cliente: subscribe, unsubscribe, presence.join, presence.leave, presence.active, presence.idle ({ "type": "presence.idle", "idle": true } cuando la pestaña lleva 10 minutos inactiva, false cuando vuelve: la persona aparece Ausente mientras todas sus pestañas están inactivas), typing (solo con el chat activado) y ping.
Límites y códigos de cierre
- Mensajes de hasta 4 KB, sin binario, como máximo 5 por segundo.
- El servidor envía
pingcada 25 segundos. - Para reconectar, espera un tiempo aleatorio y creciente, y pide un ticket nuevo.
| Código | Qué significa | Qué hacer |
|---|---|---|
4004 | Sesión terminada | No reconectes con la misma sesión |
4009 | Reemplazada: la persona abrió más de 10 conexiones y se cerró la más antigua | Nada, si es la pestaña antigua |
4029 | Servidor lleno | Conéctate más tarde y usa las rutas mientras tanto |
1008 | Demasiados mensajes | Baja el ritmo |
1001 | Servidor reiniciándose | Reconecta |