Skip to content

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

AspectoValor
SecretOLP_JWT_SECRET (mesmo do sistema)
Expiração5 minutos
Claimssub, principal_role, escola_id, role: 'authenticated', scope: 'realtime_only'
Sem jtiNão precisa de revogação (vida curta)
Sem nome_completoNão é necessário para RLS
EntregaVia JSON response do /me
GuardextractAuthenticatedUser 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

CanalTipoEscopo
presence:olp_teamPresenceAdmin + Especialista
presence:escola_{escola_id}PresenceUsuários da mesma escola
realtime:notificacoesPostgres 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

HookArquivoResponsabilidade
useRealtimeAuthsrc/hooks/useRealtimeAuth.tsToken management, refresh 4min, tab focus
useRealtimePresencesrc/hooks/useRealtimePresence.tsPresence channel (substitui polling)
useRealtimeNotificacoessrc/hooks/useRealtimeNotificacoes.tsPostgres Changes + load inicial

Arquivos Backend

ArquivoResponsabilidade
_shared/jwt-realtime.tsGera JWT Realtime de 5min
_shared/auth-helpers.tsGuard contra scope: 'realtime_only' em Edge Functions
me/index.tsRetorna 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)

  1. JWT Secret incompatível: O OLP_JWT_SECRET DEVE ser idêntico ao JWT Secret do projeto Supabase (Dashboard > Settings > API > JWT Secret). Se forem diferentes, o Realtime rejeita o token silenciosamente.
  2. Race condition: O isConnected é setado com delay de 500ms após setAuth() para dar tempo ao WebSocket de processar a autenticação.
  3. 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.