Aparência
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
| Canal | Tipo | Quem autoriza | O que carrega |
|---|---|---|---|
school.{schoolId}.monitor | privado | Operador + admin: permissão de ver o monitor ao vivo E a escola pertence ao usuário | Payload completo: StudentStateChanged, ArrivalSignalCreated, PickupConfirmed |
school.{schoolId}.queue | privado (presença) | permissão de ver os sinais de chegada | Presença dos operadores na fila (FR-042) — hoje nenhum evento publica aqui ⚠️ |
school.{schoolId}.public-monitor | público | A verificação no canal é defense-in-depth (casa a escola vinculada ao token); o gate real é o token do painel público | Cartões privacy-safe (5 campos, SC-008): StudentStateChanged, ArrivalSignalCreated, PickupConfirmed |
responsible.{userId}.school.{schoolId} | privado | Só o próprio usuário cujo responsável está Ativo naquela escola | Payload 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 fio | Canais | Payload (resumo) | Contraparte pública |
|---|---|---|---|---|
StudentStateChanged | StudentStateChanged | …monitor + N × responsible.… | id do aluno, nome, turmas, foto, estado de retirada, versão do estado, ponto de retirada, id do sinal de chegada, motivo | PublicStudentStateChanged (5 campos) |
ArrivalSignalCreated | ArrivalSignalCreated | …monitor | contexto de chegada: alunos c/ foto, coordenadas reportadas, verificação de identidade | PublicArrivalSignalCreated (1 cartão por aluno/irmão) |
PickupConfirmed | PickupConfirmed | …monitor | id da retirada, id do aluno, método, id do sinal de chegada, quando foi confirmada | PublicPickupConfirmed (só card_id) |
StudentRosterChanged | StudentRosterChanged | responsible.… | 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 guardauth:sanctum), o companheiro autenticado por token do/broadcasting/authde 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 emGET /api/v1/me/monitor/credentials(escopo porX-School-Id); painel público emGET /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
| Ponto | Como estava | Veredito | Esforço |
|---|---|---|---|
| Estado dos alunos (app do responsável) | Push FCM em foreground + refetch ao focar | 🟢 Precisa | Baixo·Médio — assinar o canal no mobile |
| Notificações / alertas (inbox) | notificação no banco + push FCM; feed só recarrega ao focar | 🟢 Precisa | Médio — ativar canal de inbox (stub) |
| "Estou chegando" (acompanhar retirada) | GET /arrival-signals/active em polling de 15s | 🟢 Precisa · #1 | Baixo·Médio — mesmo canal do responsável |
| Detalhe do aluno | Busca única na montagem | 🟡 Avaliar — sim | Grátis* (se o canal existir) |
Vínculo de alunos (StudentRosterChanged) | Backend já emite; mobile não escutava | 🟡 Marginal | Grátis* — vem junto do canal |
| Portaria (kanban PWA) | Já realtime via Echo/Reverb | ✅ Feito | — |
| Painel público / kiosk | Já realtime (canal público, LGPD-safe) | ✅ Feito | — |
Presença de operadores (…queue) | Canal existe, nenhum evento publica | ⚪ Não agora | Médio — baixo retorno |
Anúncios (user.{id}.inbox) | Autorização stub; feature não existe | ⚪ Não agora | Alto — 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
- ciclo de retirada — a máquina de estados que gera os eventos
- monitor de portaria — consumidor de
school.{id}.monitor - app do responsável — consumidor de
responsible.{u}.school.{s} - painel público — consumidor do canal público
- autenticação e autorização — o handshake Sanctum e os Papéis por trás da autorização de canal
- runbook de tempo real — Reverb, filas, alarmes (complemento operacional)