# 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 fechamento.

O app recebe as mudanças na hora por um WebSocket: o sino, o chat, o "Online agora", quem está em cada tela, o banner de evento ao vivo, os números das campanhas. Qualquer membro da comunidade, de qualquer papel, abre o socket, **com o chat ligado ou desligado**.

## Abrir a conexão

1. Peça um ticket com `POST /api/realtime/ticket`, com o token e o `X-CommunityId` (ver [Autenticação](/api/autenticacao) e [X-CommunityId](/api/x-community-id)). O ticket é de **uso único**, vale **60 segundos** e fica preso à pessoa, à comunidade e à sessão.
2. Abra `wss://<api>/api/realtime/socket` (o `data.path`) passando o ticket como **segundo subprotocolo**, depois de `memberfy-realtime` (o `data.protocols`). O servidor responde com `memberfy-realtime`.

```js
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 na URL (na query, ele é ignorado). Sem ticket válido, a conexão é recusada antes de virar WebSocket (401). Trocar de comunidade fecha o socket: peça outro ticket. Sem o serviço de tempo real, o ticket volta `503`, e o app segue buscando pelas rotas.

`POST /api/chat/ticket` e `/api/chat/socket` continuam funcionando como **outros nomes** destes por um deploy e depois saem: use os novos.

## Salas

Toda conexão já entra em três salas:

| Sala | Quem |
|---|---|
| `me` | A própria pessoa |
| `community` | Todos da comunidade |
| `staff` | Só a equipe (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 o post |

```json
{ "type": "subscribe", "rooms": ["space:5b0e7c1a-…", "item:event:9d2f…"] }
{ "type": "unsubscribe", "rooms": ["space:5b0e7c1a-…"] }
```

Também há salas para os editores, só para os papéis que podem gravar ali: `item:course:<id>`, `item:form:<id>` (a equipe e quem gerencia o formulário; um membro quando está publicado e ele 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) e `item:emailOverride:<emailKey>` (e-mail padrão, proprietário e administrador). E as salas de área, uma por tela de seção: `area:home` (todos), `area:members`, `area:campaigns`, `area:emails`, `area:workflows` (equipe), `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 conexão (as automáticas não contam). A resposta é `subscribed` com as salas aceitas e um `subscribe.denied` por sala recusada, **sem motivo**. Quando o acesso da pessoa muda (saiu, papel novo, grupo, acesso do espaço), o servidor reavalia as salas, manda `subscribe.denied` das que ela perdeu e depois `access.changed`.

## Avisos de mudança (`invalidate`)

```json
{ "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á na tela 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: seção ou espaço criado, editado (nome, ícone, endereço, seçã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 em sequência viram um aviso só, em até 1 segundo |

Os avisos de evento chegam só a quem vê aquele espaço; evento privado ou em moderação, só à equipe.

## Quem está aqui

A presença de área (os rostos no topo de cada tela, ver [Quem está aqui e edição em equipe](/conceitos/presenca-e-edicao-em-equipe)) usa as mesmas salas.

```json
{ "type": "presence.join", "room": "area:campaigns", "editing": false }
{ "type": "presence.leave", "room": "area:campaigns" }
{ "type": "presence.active" }
```

- `presence.join` ao abrir a tela: a sala é avaliada como no `subscribe` (e assinada, se ainda não estava), no máximo **3 lugares** por conexão. A resposta é `presence.joined` (`room`, `editing`) seguido de um retrato `presence`, ou `presence.denied` (`room`), sem motivo.
- `presence.leave` ao sair. Não há heartbeat: o ping do servidor mantém o lugar, que some 45 segundos depois da última resposta; fechar a conexão tira na hora.
- `presence.active` quando 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 conexão:

```json
{
  "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"
}
```

- Você **nunca** está na própria lista. `members` traz no máximo 5 pessoas (editando e ativas primeiro, depois as ativas, depois as ociosas); `count` é o total das outras pessoas na sala.
- **Quem só lê:** nas salas de espaço e de item, quem não está editando ali recebe só `memberId`, `name`, `avatarUrl` e `state`, sem `editing`, `lastActiveAt` nem `editingCount`, para preservar quem edita. Quem edita o mesmo item e as salas de área da equipe recebem o retrato completo.
- `state: "idle"`: conectada, 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 (`members` vazio, `count` 0), inclusive a equipe. Só quem edita o mesmo item continua vendo os outros editores; o `item.saved` e o `409` de versão não mudam.

### Edição e `item.saved`

Num editor (evento, campanha, e-mail da biblioteca, workflow, curso, formulário, pesquisa, modelo de certificado, e-mail padrão e as configurações, em `area:settings`), entre com `"editing": true`. Só vale para quem pode gravar ali (a equipe e, no formulário, quem o gerencia). Quem edita aparece com `editing: true` para os **outros editores** da sala, mesmo com o status do chat desligado; quem só lê a página não fica sabendo. `editingCount` conta só quem edita ativo.

A cada gravação, a sala recebe, para a equipe e para quem está editando:

```json
{ "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 você tem para ignorar a sua própria gravação. `itemType`: `event`, `campaign`, `email`, `workflow`, `course` (inclusive módulos e aulas), `form`, `survey`, `certificateTemplate`, `emailOverride` (`itemId` = o `emailKey`) e, na sala `area:settings` (`itemId` = a comunidade), `settings`, `chatSettings`, `notificationSettings`, `messagingSettings` (SMS e WhatsApp), `creditSettings`, `businessInformation` e `payoutSettings`.

As gravações aceitam a versão lida e respondem `409` quando outra veio antes: ver [Conflito de versão](/api/respostas-e-erros#conflito-de-versao).

## Outros eventos

Do servidor, em JSON `{ type, data }`: `ready` (com `profileId`, `communityId`, `chatAvailable`, `chatStatus` (o status escolhido no chat), `chatEnabled`, `rooms`, `maxRooms` e `pingIntervalMs`), `notification.new`, `notification.read` e `notification.removed` (o sino, em toda comunidade), `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ó eles param.

Do cliente: `subscribe`, `unsubscribe`, `presence.join`, `presence.leave`, `presence.active`, `presence.idle` (`{ "type": "presence.idle", "idle": true }` quando a aba fica 10 minutos parada, `false` quando volta: a pessoa aparece **Ausente** quando todas as abas dela estão paradas), `typing` (só com o chat ligado) e `ping`.

## Limites e códigos de fechamento

- Quadros de até **4 KB**, sem binário, no máximo **5 por segundo**.
- O servidor manda `ping` a cada 25 segundos.
- Para reconectar, espere um tempo aleatório e crescente, e peça um **ticket novo**.

| Código | O que significa | O que fazer |
|---|---|---|
| `4004` | Sessão encerrada | Não reconecte com a mesma sessão |
| `4009` | Substituída: a pessoa abriu mais de 10 conexões, e a mais antiga fechou | Nada, se for a aba antiga |
| `4029` | Servidor cheio | Conecte de novo mais tarde e use as rotas enquanto isso |
| `1008` | Quadros demais | Diminua o ritmo |
| `1001` | Servidor reiniciando | Reconecte |

## Relacionados

- [Autenticação](/api/autenticacao)
- [X-CommunityId](/api/x-community-id)
- [Mensagens](/membros/mensagens)
