Tempo real (WebSocket)
O WebSocket da plataforma - o ticket de uso único, o subprotocolo memberfy-realtime, as salas automáticas e as pedidas, os avisos invalidate, os limites e os códigos de fecho.
A app recebe as mudanças de imediato por um WebSocket: o sino, o chat, o "Online agora", quem está em cada ecrã, a faixa de evento ao vivo, os números das campanhas. Qualquer membro da comunidade, de qualquer função, abre o socket, com o chat ligado ou desligado.
Abrir a ligação
- Peça um ticket com
POST /api/realtime/ticket, com o token e oX-CommunityId(ver Autenticação e X-CommunityId). O ticket é de uso único, vale 60 segundos e fica preso à pessoa, à comunidade e à sessão. - Abra
wss://<api>/api/realtime/socket(odata.path) passando o ticket como segundo subprotocolo, depois dememberfy-realtime(odata.protocols). O servidor responde commemberfy-realtime.
const { data } = await api.post('/api/realtime/ticket'); // com Authorization e X-CommunityId
const ws = new WebSocket(`wss://api.memberfy.net${data.path}`, data.protocols);
O ticket nunca vai no URL (na query, é ignorado). Sem ticket válido, a ligação é recusada antes de se tornar WebSocket (401). Mudar de comunidade fecha o socket: peça outro ticket. Sem o serviço de tempo real, o ticket devolve 503, e a app continua a ir buscar pelas rotas.
POST /api/chat/ticket e /api/chat/socket continuam a funcionar como outros nomes destes durante um deploy e depois saem: use os novos.
Salas
Toda a ligação entra logo em três salas:
| Sala | Quem |
|---|---|
me | A própria pessoa |
community | Todos da comunidade |
staff | Só a equipa (proprietário, administrador, moderador e financeiro) |
As outras são pedidas pelo cliente, com a mesma regra do GET correspondente, sempre nesta comunidade:
| Sala | Quem entra |
|---|---|
space:<id> | Quem abre o espaço |
item:event:<id> | Quem abre o evento |
item:post:<id> | Quem abre a publicação |
{ "type": "subscribe", "rooms": ["space:5b0e7c1a-…", "item:event:9d2f…"] }
{ "type": "unsubscribe", "rooms": ["space:5b0e7c1a-…"] }
Há também salas para os editores, só para as funções que podem gravar ali: item:course:<id>, item:form:<id> (a equipa e quem gere o formulário; um membro quando está publicado e abre o espaço), item:campaign:<id>, item:email:<id> (e-mail da biblioteca), item:workflow:<id>, item:survey:<id> (proprietário e administrador), item:certificateTemplate:<id> (quem emite certificados no espaço), item:emailOverride:<emailKey> (e-mail predefinido, proprietário e administrador). E as salas de área, uma por ecrã de secção: area:home (todos), area:members, area:campaigns, area:emails, area:workflows (equipa), area:moderation (proprietário, administrador e moderador), area:analytics, area:dashboard, area:settings (proprietário e administrador) e area:finance (proprietário, administrador e financeiro).
No máximo 20 salas por ligação (as automáticas não contam). A resposta é subscribed com as salas aceites e um subscribe.denied por sala recusada, sem motivo. Quando o acesso da pessoa muda (saiu, nova função, grupo, acesso do espaço), o servidor reavalia as salas, manda subscribe.denied das que perdeu e depois access.changed.
Avisos de mudança (invalidate)
{ "type": "invalidate", "room": "community", "topic": "event.live", "ids": ["9d2f…"], "at": "2026-10-23T14:05:00.000Z" }
O aviso traz só os ids e o tópico, nunca título, texto, valor ou nome: ao recebê-lo, refaça o GET do que está no ecrã pelas rotas de sempre.
| Tópico | Sala | Quando |
|---|---|---|
event.live | community, space:<id>, item:event:<id> | Um evento começou ou terminou a transmissão |
events | community, space:<id>, item:event:<id> | Um evento foi criado, editado ou removido |
analytics.online | staff (proprietário e administrador) | O "Online agora" mudou (no máximo 1 a cada 5 segundos) |
campaigns.stats | staff | Um envio de campanha terminou |
menu.changed | community | O menu mudou: secção ou espaço criado, editado (nome, ícone, endereço, secção, acesso, visibilidade), removido ou reordenado. Sem ids e sem nenhum dado do menu: cada pessoa refaz o GET /api/sections com as próprias permissões. Mudanças seguidas resultam num só aviso, em até 1 segundo |
Os avisos de evento chegam só a quem vê esse espaço; evento privado ou em moderação, só à equipa.
Quem está aqui
A presença de área (os rostos no topo de cada ecrã, ver Quem está aqui e edição em equipa) usa as mesmas salas.
{ "type": "presence.join", "room": "area:campaigns", "editing": false }
{ "type": "presence.leave", "room": "area:campaigns" }
{ "type": "presence.active" }
presence.joinao abrir o ecrã: a sala é avaliada como nosubscribe(e subscrita, se ainda não estava), no máximo 3 lugares por ligação. A resposta épresence.joined(room,editing) seguido de um retratopresence, oupresence.denied(room), sem motivo.presence.leaveao sair. Não há heartbeat: o ping do servidor mantém o lugar, que desaparece 45 segundos depois da última resposta; fechar a ligação tira-o de imediato.presence.activequando há interação (tecla, clique, foco), no máximo a cada 10 segundos. Entrar conta como atividade.
A cada mudança (no máximo 1 por segundo por sala), a sala recebe um retrato montado para cada ligação:
{
"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"
}
- Nunca está na sua própria lista.
memberstraz no máximo 5 pessoas (a editar e ativas primeiro, depois as ativas, depois as inativas);counté o total das outras pessoas na sala. - Quem só lê: nas salas de espaço e de item, quem não está a editar ali recebe só
memberId,name,avatarUrlestate, semediting,lastActiveAtnemeditingCount, para preservar quem edita. Quem edita o mesmo item e as salas de área da equipa recebem o retrato completo. state: "idle": ligada, sem atividade há 30 segundos ou mais. Continua na lista.- Privacidade: quem desligou Disponível para conversar não aparece nem conta para os outros, mas vê quem está. Um bloqueio no chat esconde as duas pessoas uma da outra.
- Chat da comunidade desligado: as salas de área e quem só lê não recebem ninguém (
membersvazio,count0), incluindo a equipa. Só quem edita o mesmo item continua a ver os outros editores; oitem.savede o409de versão não mudam.
Edição e item.saved
Num editor (evento, campanha, e-mail da biblioteca, workflow, curso, formulário, inquérito, modelo de certificado, e-mail predefinido e as definições, em area:settings), entre com "editing": true. Só vale para quem pode gravar ali (a equipa e, no formulário, quem o gere). Quem edita aparece com editing: true aos outros editores da sala, mesmo com o estado do chat desligado; quem só lê a página não fica a saber. editingCount conta só quem edita ativo.
A cada gravação, a sala recebe, para a equipa e para quem está a editar:
{ "type": "item.saved", "topic": "item.saved", "room": "item:campaign:<id>", "itemType": "campaign", "itemId": "<id>", "by": { "memberId": "…", "name": "Maria" }, "updatedAt": "<nova versão>", "at": "…" }
Compare updatedAt com a versão que tem para ignorar a sua própria gravação. itemType: event, campaign, email, workflow, course (incluindo módulos e aulas), form, survey, certificateTemplate, emailOverride (itemId = emailKey) e, na sala area:settings (itemId = a comunidade), settings, chatSettings, notificationSettings, messagingSettings (SMS + WhatsApp), creditSettings, businessInformation, payoutSettings.
As gravações aceitam a versão lida e respondem 409 quando outra veio antes: ver Conflito de versão.
Outros eventos
Do servidor, em JSON { type, data }: ready (com profileId, communityId, chatAvailable, chatStatus (o estado escolhido no chat), chatEnabled, rooms, maxRooms e pingIntervalMs), notification.new, notification.read e notification.removed (o sino, em todas as comunidades), chat.settings, session.revoked e pong. Os eventos do chat só chegam com o chat ligado; com o chat desligado, o socket continua de pé e só esses param.
Do cliente: subscribe, unsubscribe, presence.join, presence.leave, presence.active, presence.idle ({ "type": "presence.idle", "idle": true } quando o separador fica 10 minutos parado, false quando volta: a pessoa aparece Ausente quando todos os separadores dela estão parados), typing (só com o chat ligado) e ping.
Limites e códigos de fecho
- Quadros até 4 KB, sem binário, no máximo 5 por segundo.
- O servidor manda
pinga cada 25 segundos. - Para voltar a ligar, espere um tempo aleatório e crescente e peça um ticket novo.
| Código | O que significa | O que fazer |
|---|---|---|
4004 | Sessão terminada | Não volte a ligar com a mesma sessão |
4009 | Substituída: a pessoa abriu mais de 10 ligações, e a mais antiga fechou | Nada, se for o separador antigo |
4029 | Servidor cheio | Volte a ligar mais tarde e use as rotas entretanto |
1008 | Quadros a mais | Abrande o ritmo |
1001 | Servidor a reiniciar | Volte a ligar |