Skip to content

Tempo real — canais, eventos e broadcast

O que é este documento — a arquitetura autoritativa do tempo real do ionCLASS: os 4 canais de broadcast, os eventos que trafegam em cada um, quem os autoriza, e o modelo de reconciliação por versão que mantém as quatro superfícies em sincronia. Inclui, ao final, a decisão de produto sobre o que vale virar tempo real (o documento original que deu origem a este). O complemento operacional/infra (Reverb, filas, alarmes) é o runbook de tempo real.


1. Como funciona, em uma frase

Quase todo o "tempo real" do produto é uma coisa só: a transição de estado de um aluno na retirada (o estado de retirada), empurrada pelo operador ou pelo hardware de controle de acesso, e transmitida de um único ponto central — o difusor de estado do monitor. A partir daí o mesmo fato se ramifica em eventos privados (payload completo) e públicos (payload LGPD-safe), cada um no seu canal.

  transição de estado (avanço do operador / resolução da corrida / reversão automática)


                      difusor de estado do monitor
                    │                                  │
                    ▼                                  ▼ (só se há cartão/sinal ativo)
         StudentStateChanged                 PublicStudentStateChanged
        (canal privado, completo)             (canal público, 5 campos)
          │                    │                        │
          ▼                    ▼                        ▼
   school.{id}.monitor   responsible.{u}.school.{s}   school.{id}.public-monitor
   (portaria + admin)    (app do responsável)         (painel público / kiosk)

Os eventos são enfileirados — vão para a fila Redis e saem pelo worker de forma assíncrona, sem bloquear a ação que os disparou.


2. Os 4 canais

CanalTipoQuem autorizaO que carrega
school.{schoolId}.monitorprivadoOperador + admin: permissão de ver o monitor ao vivo E a escola pertence ao usuárioPayload completo: StudentStateChanged, ArrivalSignalCreated, PickupConfirmed
school.{schoolId}.queueprivado (presença)permissão de ver os sinais de chegadaPresença dos operadores na fila (FR-042) — hoje nenhum evento publica aqui ⚠️
school.{schoolId}.public-monitorpúblicoA verificação no canal é defense-in-depth (casa a escola vinculada ao token); o gate real é o token do painel públicoCartões privacy-safe (5 campos, SC-008): StudentStateChanged, ArrivalSignalCreated, PickupConfirmed
responsible.{userId}.school.{schoolId}privadoSó o próprio usuário cujo responsável está Ativo naquela escolaPayload completo StudentStateChanged + StudentRosterChanged

Existe ainda um user.{userId}.inbox (canal privado) reservado a comunicados (FR-061), mas sua autorização hoje é stub (nega tudo) — a feature de anúncios ainda não existe. Não é um canal ativo. ⚠️

2.1 school.{id}.monitor — a portaria e o admin

Uma regra só para todas as audiências internas (painel e PWA do operador): ter a permissão de ver o monitor ao vivo e enxergar a escola. A isolação por tenant segue o mesmo escopo de escola do usuário (um operador é resolvido pela sua escola). O antigo bypass "é operador → pode" foi removido por ser escalação de privilégio (deixava um operador sem Papel transmitir o quadro que o painel negaria à mesma pessoa).

2.2 school.{id}.public-monitor — o painel público

Canal público de propósito: os eventos só carregam o cartão de 5 campos (card_id, student_first_name, class_name, status, since). Como nenhum dado sensível trafega, o controle de acesso não está no canal e sim no token do painel público, que protege a página e os endpoints de credenciais. A verificação no canal é só defesa em profundidade: autoriza apenas quando o contexto vinculado ao token bate com a escola pedida. O card_id é sempre o id do sinal de chegada, para que criação/atualização/fade mirem o mesmo cartão. Ver painel público.

2.3 responsible.{u}.school.{s} — o app do responsável

Escopo por (usuário, escola) para que o app feche/abra a assinatura limpo quando o responsável troca de escola ativa. Autoriza só o próprio usuário, e só se o responsável está Ativo naquela escola — um responsável bloqueado (status ≠ Ativo) não assina. StudentStateChanged é transmitido para um canal por responsável vinculado que tenha conta no app; StudentRosterChanged avisa quando a lista de alunos muda (entra/sai/edita). Ver app do responsável.


3. Os eventos

Cada evento privado tem uma contraparte pública que usa o mesmo nome no fio — assim o frontend do kiosk trata os dois uniformemente, mudando só o payload.

Evento (privado)Nome no fioCanaisPayload (resumo)Contraparte pública
StudentStateChangedStudentStateChanged…monitor + N × responsible.…id do aluno, nome, turmas, foto, estado de retirada, versão do estado, ponto de retirada, id do sinal de chegada, motivoPublicStudentStateChanged (5 campos)
ArrivalSignalCreatedArrivalSignalCreated…monitorcontexto de chegada: alunos c/ foto, coordenadas reportadas, verificação de identidadePublicArrivalSignalCreated (1 cartão por aluno/irmão)
PickupConfirmedPickupConfirmed…monitorid da retirada, id do aluno, método, id do sinal de chegada, quando foi confirmadaPublicPickupConfirmed (só card_id)
StudentRosterChangedStudentRosterChangedresponsible.…ação (vinculado/desvinculado/removido/atualizado), aluno, escola — app faz refetch ao receber

O difusor de estado do monitor centraliza o par: emite sempre o StudentStateChanged privado e, só quando há um cartão (sinal de chegada ativo ancorando o cartão público), também o PublicStudentStateChanged. ArrivalSignalCreated/PickupConfirmed e suas contrapartes públicas são disparados nos seus próprios pontos (criação do sinal, resolução da retirada).


4. Handshake e reconciliação

  • Autenticação Sanctum no broadcast. O handshake de canais privados usa POST /api/broadcasting/auth (protegido pelo guard auth:sanctum), o companheiro autenticado por token do /broadcasting/auth de sessão (web). Ambos compartilham as mesmas definições de autorização de canais.
  • Credenciais para os clientes. Cada cliente pega os parâmetros do Reverb + o nome do seu canal por um endpoint dedicado: operador em GET /api/v1/operator/monitor/credentials; responsável em GET /api/v1/me/monitor/credentials (escopo por X-School-Id); painel público em GET /api/v1/public-monitor/{school}/credentials (via token).
  • Reconciliação por versão que nunca regride. A versão do estado é o eixo de verdade: o cliente aplica um evento só se a versão for maior que a que ele já tem. Um evento fora de ordem (rede, refetch, push) nunca faz o estado voltar. O padrão é best-effort: update otimista + reconciliação por versão, e se o socket cai, cai para refetch (a portaria já faz exatamente isso; o mobile herda o padrão). Detalhe do ciclo de estados: ciclo de retirada.

5. Decisão: o que virou tempo real

Esta seção preserva a avaliação de produto que originou o documento: quais pontos do ionCLASS valem funcionar em tempo real, pesando ganho de UX × complexidade × custo de operação. Varredura original: 2026-08-07.

Conclusão: o backend já transmite tudo. O gargalo histórico era o app do responsável (mobile), que não falava WebSocket — se virava com push FCM (só em foreground), refetch ao focar e um polling de 15s. O custo dos itens prioritários é majoritariamente cliente, não infraestrutura.

5.1 Matriz de decisão

PontoComo estavaVereditoEsforço
Estado dos alunos (app do responsável)Push FCM em foreground + refetch ao focar🟢 PrecisaBaixo·Médio — assinar o canal no mobile
Notificações / alertas (inbox)notificação no banco + push FCM; feed só recarrega ao focar🟢 PrecisaMédio — ativar canal de inbox (stub)
"Estou chegando" (acompanhar retirada)GET /arrival-signals/active em polling de 15s🟢 Precisa · #1Baixo·Médio — mesmo canal do responsável
Detalhe do alunoBusca única na montagem🟡 Avaliar — simGrátis* (se o canal existir)
Vínculo de alunos (StudentRosterChanged)Backend já emite; mobile não escutava🟡 MarginalGrátis* — vem junto do canal
Portaria (kanban PWA)Já realtime via Echo/Reverb✅ Feito
Painel público / kioskJá realtime (canal público, LGPD-safe)✅ Feito
Presença de operadores (…queue)Canal existe, nenhum evento publica⚪ Não agoraMédio — baixo retorno
Anúncios (user.{id}.inbox)Autorização stub; feature não existe⚪ Não agoraAlto — depende de construir a feature

Os três "Precisa" compartilham a mesma solução: um cliente WebSocket no mobile assinando o canal do responsável (responsible.{u}.school.{s}). Detalhe do aluno e vínculo vêm quase de graça no pacote.

5.2 Por que melhora a experiência

No domínio da saída escolar, tempo real não é enfeite — é a própria promessa. O responsável quer ver o filho passar de aguardando retirada → em preparação → pronto → retirado no instante em que a portaria move o cartão. Isso mata o snapshot velho (até 15 s de tela desatualizada no polling), reduz ansiedade e ligações para a secretaria, e alinha as três telas que hoje o responsável assiste "atrasado".

5.3 A complexidade real está no cliente, não no backend

  • Backend — praticamente pronto. Reverb, canais, autorização por tenant/status e eventos enfileirados já existem. O trabalho novo foi só o endpoint de credenciais do responsável (me/monitor/credentials), espelhando o do operador.
  • Mobile — o grosso do esforço. Ciclo de vida de uma conexão persistente num celular: conectar em foreground, desconectar em background (bateria), reconectar com backoff, e reconciliar com refetch ao reconectar para não perder a janela offline.
  • Dois caminhos, não um. Push FCM funciona com o app fechado; WebSocket só com o app aberto. Mantêm-se os dois, deduplicando pela versão do estado (nunca regredir) — o inbox no banco é a fonte da verdade.

5.4 Desempenho e monitoramento — o horário de saída

O risco de performance está concentrado num único momento: o horário de saída, quando conexões e eventos explodem juntos. Milhares de sockets vivos pressionam memória/file descriptors do Reverb (escalar exige ativar o modo de escalonamento do Reverb com Redis pub/sub, hoje desligado); o fan-out em rajada (N alunos × M responsáveis + portaria + kiosk) precisa vazar rápido da fila. Como o design é best-effort, uma queda do Reverb não gera erro na tela — degrada em silêncio. Por isso o monitoramento é obrigatório. O detalhamento de infra, métricas e alarmes (a regra de ouro: alertar quando a fila cresce) vive no runbook de tempo real.


Ver também

Documentação do ionCLASS