Supabase Realtime — Arquitetura
Contexto
O Supabase Realtime exige um JWT acessível via JavaScript (supabase.realtime.setAuth(token)). Nosso JWT principal (olp_auth) vive em um cookie HttpOnly — inacessível ao JS por design (proteção XSS).
Solução: Um JWT secundário de curta duração (scope: 'realtime_only'), entregue via JSON (não cookie), exclusivamente para autenticar canais Realtime.
Token Realtime
| Aspecto | Valor |
|---|---|
| Secret | OLP_JWT_SECRET (mesmo do sistema) |
| Expiração | 5 minutos |
| Claims | sub, principal_role, escola_id, role: 'authenticated', scope: 'realtime_only' |
Sem jti | Não precisa de revogação (vida curta) |
Sem nome_completo | Não é necessário para RLS |
| Entrega | Via JSON response do /me |
| Guard | extractAuthenticatedUser rejeita tokens com scope: 'realtime_only' |
Por que incluir principal_role?
As RLS policies dos canais Presence dependem de principal_role e escola_id para filtrar visibilidade ("mundos"). Sem essas claims, o Realtime Presence não funcionaria corretamente.
Por que 5 minutos?
Se vazado via XSS, a janela de exploração é mínima (vs 8h do token principal). O token não pode chamar Edge Functions (guard no extractAuthenticatedUser).
Diagrama de Fluxo
Frontend Backend (Edge Functions)
│ │
│ 1. GET /me (cookie olp_auth) │
│ ──────────────────────────────────► │
│ │ Valida cookie, gera realtime_token
│ 2. { user, realtime_token } │ (JWT 5min, claims mínimas)
│ ◄────────────────────────────────── │
│ │
│ 3. supabase.realtime.setAuth( │
│ realtime_token) │
│ 4. Subscribe channels: │
│ - presence:escola_{id} │
│ - realtime:notificacoes │
│ ──────── WebSocket ───────────────►│ Supabase Realtime (RLS via JWT)
│ │
│ 5. A cada 4min: refresh via /me │
│ action: 'realtime_token' │
│ ──────────────────────────────────► │ Novo realtime_token (~5ms)
│ │Canais
| Canal | Tipo | Escopo |
|---|---|---|
presence:olp_team | Presence | Admin + Especialista |
presence:escola_{escola_id} | Presence | Usuários da mesma escola |
realtime:notificacoes | Postgres Changes (INSERT) | usuario_id = auth.uid() |
Tab Focus Management
Para proteger o limite de 200 conexões simultâneas do free tier:
- Aba em background > 5 minutos →
supabase.removeAllChannels()+ para refresh - Aba volta ao foco → reconecta canais + busca token novo
- Último estado de presença/notificações fica em cache (sem flash de "vazio")
Hooks Frontend
| Hook | Arquivo | Responsabilidade |
|---|---|---|
useRealtimeAuth | src/hooks/useRealtimeAuth.ts | Token management, refresh 4min, tab focus |
useRealtimePresence | src/hooks/useRealtimePresence.ts | Presence channel (substitui polling) |
useRealtimeNotificacoes | src/hooks/useRealtimeNotificacoes.ts | Postgres Changes + load inicial |
Arquivos Backend
| Arquivo | Responsabilidade |
|---|---|
_shared/jwt-realtime.ts | Gera JWT Realtime de 5min |
_shared/auth-helpers.ts | Guard contra scope: 'realtime_only' em Edge Functions |
me/index.ts | Retorna realtime_token + action leve realtime_token |
Segurança
- Token Realtime não pode chamar Edge Functions (guard explícito)
- Sem blacklist — vida curta (5min), sem
jti - Logout invalida cookie
olp_auth→ refresh falha → WebSocket desconecta em ≤4min - RLS continua funcionando normalmente via claims do token
Troubleshooting
Presença não funciona (CHANNEL_ERROR)
- JWT Secret incompatível: O
OLP_JWT_SECRETDEVE ser idêntico ao JWT Secret do projeto Supabase (Dashboard > Settings > API > JWT Secret). Se forem diferentes, o Realtime rejeita o token silenciosamente. - Race condition: O
isConnectedé setado com delay de 500ms apóssetAuth()para dar tempo ao WebSocket de processar a autenticação. - Logs de debug: Os hooks emitem logs com prefixo
[Realtime:Auth],[Realtime:Presence]e[Realtime:Notificacoes]no console do browser.
CSP bloqueando WebSocket
O connect-src do CSP em index.html DEVE incluir wss://*.supabase.co além de https://*.supabase.co. WebSocket usa protocolo wss://, que não é coberto por https://.
Sintoma: CHANNEL_ERROR + log do browser mencionando CSP violation.
Tipo Notificacao unificado
A interface Notificacao vive em src/types/notificacao.ts e é re-exportada por useNotificacoes e useRealtimeNotificacoes. Todos os componentes importam de @/types/notificacao.