Alternar tema

Supabase Realtime na prática: comparação entre três modos e desenvolvimento de aplicações colaborativas

Easton editorial illustration: three-option fit selector

Fiquei olhando para o indicador “digitando…” na tela e atualizei a página do chat pela décima sétima vez. A pessoa do outro lado estava claramente online, mas as mensagens simplesmente não apareciam. Criar uma aplicação realmente “em tempo real” é muito mais difícil do que parece.

Encontrei muitos problemas ao trabalhar com WebSocket: é preciso reconectar quando a conexão cai, tratar a sincronização de estado e projetar a transmissão de mensagens. Quando usei o Supabase Realtime no ano passado, percebi que era possível deixar todo esse trabalho para o serviço. O Supabase oferece três modos em tempo real: Postgres Changes monitora alterações no banco de dados, Presence acompanha o estado dos usuários e Broadcast transmite mensagens temporárias. Cada modo serve para situações diferentes. A escolha certa facilita muito o trabalho; a errada cria novos problemas.

Neste artigo, vou explicar em detalhes quando usar cada modo, como escrever o código e como configurar as políticas de segurança RLS. No final, vamos combinar os três em uma aplicação completa de chat colaborativo.

Comparação entre os três principais recursos do Supabase Realtime

Primeiro, a conclusão: cada modo tem uma função própria e não deve ser usado indiscriminadamente.

Postgres Changes monitora alterações no banco de dados. É indicado para dados que precisam de persistência, como mensagens de chat, notificações e status de pedidos. Os dados ficam no banco, enquanto o cliente apenas assina as alterações.

Presence acompanha o estado online dos usuários. É indicado para mostrar quem está online, indicadores de digitação e a posição do cursor em uma edição colaborativa. Os dados não ficam no banco: permanecem em memória e desaparecem quando o usuário se desconecta.

Broadcast transmite mensagens temporárias. É indicado para o movimento do cursor em uma tela, posições em tempo real em jogos e a sincronização de operações temporárias. A diferença em relação ao Presence é que Broadcast foi feito para envios de alta frequência, enquanto Presence é voltado à sincronização de estado.

A tabela abaixo resume:

ModoOnde os dados ficamCasos de uso típicosPersistência
Postgres ChangesPostgreSQLMensagens de chat, notificações e status de pedidosSim
PresenceMemória (serviço Realtime)Usuários online e indicadores de digitaçãoNão
BroadcastNão armazena (encaminhamento imediato)Movimento do cursor e posições em tempo realNão

Talvez você pense: por que não usar simplesmente Postgres Changes para tudo? Para ser sincero, também pensei assim no começo. Depois, em uma aplicação de quadro branco colaborativo, gravei no banco cada movimento do cursor e o uso de CPU disparou para 90%. Foi quando entendi que alguns dados simplesmente não precisam ser persistidos.

Para escolher o modo, lembre-se deste princípio: use Postgres Changes quando precisar consultar o histórico, Presence quando importar apenas o estado atual e Broadcast para dados temporários de alta frequência.

Postgres Changes: monitorando alterações no banco de dados

Esta seção aborda o caso mais comum: acompanhar alterações no banco. Uma nova mensagem no chat, uma mudança no status de um pedido ou uma curtida são exemplos de dados que precisam ser persistidos.

O Supabase Realtime monitora essas alterações por meio do mecanismo de logical replication (replicação lógica) do PostgreSQL. Em termos simples, sempre que ocorre uma operação INSERT, UPDATE ou DELETE no banco, o serviço Realtime consegue capturá-la e enviá-la aos clientes assinantes.

Ativando o monitoramento pelo Realtime

Primeiro, é preciso ativar a publication no banco de dados. Execute no Supabase SQL Editor:

-- Ativar a publication do Realtime
ALTER publication supabase_realtime ADD TABLE messages;

-- Para tabelas que precisam monitorar UPDATE e DELETE, é obrigatório definir REPLICA IDENTITY FULL
ALTER TABLE messages REPLICA IDENTITY FULL;

Por que definir REPLICA IDENTITY FULL? Por padrão, o PostgreSQL registra apenas a chave primária da linha alterada. Para obter os dados completos de antes e depois da alteração, como em um log de auditoria, é preciso ativar essa opção. Atenção: isso aumenta o volume de gravações no banco, portanto ative-a somente nas tabelas que realmente precisam dela.

Código de assinatura no cliente

Agora vamos ao código do cliente. O exemplo abaixo usa mensagens de chat:

import { createClient } from '@supabase/supabase-js'
import { useEffect, useState } from 'react'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

interface Message {
  id: string
  content: string
  user_id: string
  created_at: string
}

export function useRealtimeMessages(roomId: string) {
  const [messages, setMessages] = useState<Message[]>([])

  useEffect(() => {
    // Primeiro, buscar o histórico de mensagens
    const fetchMessages = async () => {
      const { data } = await supabase
        .from('messages')
        .select('*')
        .eq('room_id', roomId)
        .order('created_at', { ascending: true })

      if (data) setMessages(data)
    }

    fetchMessages()

    // Assinar novas mensagens
    const channel = supabase
      .channel(`messages:${roomId}`)
      .on(
        'postgres_changes',
        {
          event: 'INSERT',      // Monitorar apenas novas inserções
          schema: 'public',
          table: 'messages',
          filter: `room_id=eq.${roomId}`  // Filtrar uma sala específica
        },
        (payload) => {
          // payload.new contém os dados recém-inseridos
          setMessages(prev => [...prev, payload.new as Message])
        }
      )
      .subscribe()

    // Limpar a assinatura
    return () => {
      supabase.removeChannel(channel)
    }
  }, [roomId])

  return messages
}

Alguns detalhes importantes:

  1. Busque o histórico antes de assinar os incrementos: ao entrar no chat, é preciso exibir as mensagens anteriores e só então receber as novas. Esse é um erro comum; não faça apenas a assinatura sem buscar o histórico.
  2. Parâmetro filter: filtre por um campo do banco para evitar mensagens irrelevantes. A sintaxe é nome_do_campo=eq.valor.
  3. Limpeza da assinatura: lembre-se de chamar removeChannel quando o componente for desmontado para evitar vazamentos de memória.

Configuração de segurança RLS (importante!)

Muita gente ignora esta parte, mas ela é essencial em uma aplicação de produção. Por padrão, o Realtime respeita as políticas RLS (Row Level Security, ou segurança em nível de linha) da tabela. Sem uma configuração correta de RLS, o cliente pode não receber nada ou pode receber dados que não deveria acessar.

Por exemplo, considere uma tabela de chat:

-- Tabela de mensagens
CREATE TABLE messages (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  room_id uuid REFERENCES rooms(id),
  user_id uuid REFERENCES auth.users(id),
  content text NOT NULL,
  created_at timestamptz DEFAULT now()
);

-- Ativar RLS
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;

-- Permitir a consulta de mensagens das salas às quais o usuário pertence
CREATE POLICY "Users can view messages in their rooms"
ON messages FOR SELECT
USING (
  room_id IN (
    SELECT room_id FROM room_members
    WHERE user_id = auth.uid()
  )
);

-- Permitir que membros da sala enviem mensagens
CREATE POLICY "Room members can insert messages"
ON messages FOR INSERT
WITH CHECK (
  room_id IN (
    SELECT room_id FROM room_members
    WHERE user_id = auth.uid()
  )
);

As assinaturas do Realtime aplicam essas políticas automaticamente. Cada usuário recebe somente as alterações das mensagens que tem permissão para consultar. Isso é especialmente importante: não tente fazer a filtragem no frontend, pois ela não é segura.

Se a assinatura não estiver recebendo dados, verifique primeiro:

  1. Se a tabela foi adicionada à publication supabase_realtime
  2. Se as políticas RLS estão configuradas corretamente

Presence: acompanhando o estado online dos usuários

Presence é indicado para situações em que você precisa saber “quem está online agora”: mostrar a quantidade de pessoas em uma sala de chat, indicar quem está editando uma seção de um documento colaborativo ou quem está digitando. Esses dados não precisam ficar no banco; armazená-los em memória é suficiente.

Funcionamento básico

O Presence funciona assim: depois que cada cliente entra em um canal, ele chama o método track() para registrar seu próprio estado. O serviço Realtime mantém um snapshot do estado de todos os clientes. Quando alguém entra, sai ou atualiza o próprio estado, todos os assinantes recebem uma notificação.

Criando uma lista de usuários online

Veja o código de um componente que mostra os usuários online em uma sala de chat:

import { createClient } from '@supabase/supabase-js'
import { useEffect, useState } from 'react'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

interface UserPresence {
  user_id: string
  username: string
  online_at: string
}

export function useOnlineUsers(roomId: string, currentUser: { id: string; username: string }) {
  const [users, setUsers] = useState<UserPresence[]>([])

  useEffect(() => {
    const channel = supabase.channel(`room:${roomId}`, {
      config: {
        presence: {
          key: currentUser.id  // Usar o ID do usuário como key
        }
      }
    })

    channel
      .on('presence', { event: 'sync' }, () => {
        // Obter todos os usuários online durante a sincronização
        const state = channel.presenceState()
        // presenceState() retorna { [key]: [UserPresence, ...] }
        const onlineUsers = Object.values(state).flat() as UserPresence[]
        setUsers(onlineUsers)
      })
      .on('presence', { event: 'join' }, ({ newPresences }) => {
        // Um novo usuário entrou
        console.log('Usuário entrou:', newPresences)
      })
      .on('presence', { event: 'leave' }, ({ leftPresences }) => {
        // Um usuário saiu
        console.log('Usuário saiu:', leftPresences)
      })
      .subscribe(async (status) => {
        if (status === 'SUBSCRIBED') {
          // Registrar o próprio estado após a assinatura ser concluída
          await channel.track({
            user_id: currentUser.id,
            username: currentUser.username,
            online_at: new Date().toISOString()
          })
        }
      })

    return () => {
      supabase.removeChannel(channel)
    }
  }, [roomId, currentUser])

  return users
}

O uso é simples:

function ChatRoom({ roomId, currentUser }) {
  const onlineUsers = useOnlineUsers(roomId, currentUser)

  return (
    <div className="flex items-center gap-2 mb-4">
      <span className="text-sm text-gray-500">
        {onlineUsers.length} online
      </span>
      <div className="flex -space-x-2">
        {onlineUsers.map(user => (
          <div
            key={user.user_id}
            className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white text-sm"
            title={user.username}
          >
            {user.username[0]}
          </div>
        ))}
      </div>
    </div>
  )
}

Indicador de digitação

Presence também pode criar o aviso “digitando”. A ideia é atualizar o estado do usuário quando ele começa a digitar e limpar esse estado após um período de inatividade.

export function useTypingIndicator(roomId: string, currentUser: { id: string }) {
  const [typingUsers, setTypingUsers] = useState<string[]>([])
  const channelRef = useRef<RealtimeChannel | null>(null)
  const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null)

  useEffect(() => {
    const channel = supabase.channel(`room:${roomId}`)
    channelRef.current = channel

    channel
      .on('presence', { event: 'sync' }, () => {
        const state = channel.presenceState()
        const typing = Object.values(state)
          .flat()
          .filter((u: any) => u.is_typing && u.user_id !== currentUser.id)
          .map((u: any) => u.username)
        setTypingUsers(typing)
      })
      .subscribe(async (status) => {
        if (status === 'SUBSCRIBED') {
          await channel.track({
            user_id: currentUser.id,
            is_typing: false
          })
        }
      })

    return () => {
      supabase.removeChannel(channel)
    }
  }, [roomId, currentUser])

  // Chamar quando o usuário começar a digitar
  const setTyping = (isTyping: boolean) => {
    if (typingTimeoutRef.current) {
      clearTimeout(typingTimeoutRef.current)
    }

    channelRef.current?.track({
      user_id: currentUser.id,
      is_typing
    })

    // Limpar o estado de digitação automaticamente após 3 segundos
    if (isTyping) {
      typingTimeoutRef.current = setTimeout(() => {
        channelRef.current?.track({
          user_id: currentUser.id,
          is_typing: false
        })
      }, 3000)
    }
  }

  return { typingUsers, setTyping }
}

Há uma pequena armadilha aqui: não chame track() a cada tecla pressionada. Essa frequência pode causar problemas. Aplique debounce ou, como no exemplo acima, limpe o estado automaticamente alguns segundos depois que a pessoa terminar de digitar.

Broadcast: rastreamento de cursor e mensagens instantâneas

Broadcast é o mais “leve” dos três modos: as mensagens não são armazenadas nem persistidas. Depois de enviadas, são esquecidas. Por isso, ele é indicado para transmissões temporárias de alta frequência.

Exemplo de rastreamento de cursor

Aplicações como quadros brancos colaborativos e editores para várias pessoas precisam mostrar a posição do cursor de cada participante em tempo real. Broadcast é a melhor opção para esse caso: não é preciso armazenar a posição no banco nem saber “onde o cursor esteve no passado”.

import { createClient } from '@supabase/supabase-js'
import { useEffect, useState, useRef } from 'react'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

interface CursorPosition {
  user_id: string
  username: string
  x: number
  y: number
  color: string
}

export function useCursors(canvasId: string, currentUser: { id: string; username: string }) {
  const [cursors, setCursors] = useState<Record<string, CursorPosition>>({})
  const channelRef = useRef<RealtimeChannel | null>(null)

  useEffect(() => {
    const channel = supabase.channel(`canvas:${canvasId}`)
    channelRef.current = channel

    channel
      .on('broadcast', { event: 'cursor' }, ({ payload }) => {
        // Receber a posição do cursor de outros usuários
        if (payload.user_id !== currentUser.id) {
          setCursors(prev => ({
            ...prev,
            [payload.user_id]: payload
          }))
        }
      })
      .subscribe()

    return () => {
      supabase.removeChannel(channel)
    }
  }, [canvasId, currentUser])

  // Enviar a posição do próprio cursor
  const sendCursor = (x: number, y: number) => {
    channelRef.current?.send({
      type: 'broadcast',
      event: 'cursor',
      payload: {
        user_id: currentUser.id,
        username: currentUser.username,
        x,
        y,
        color: getUserColor(currentUser.id)
      }
    })
  }

  return { cursors, sendCursor }
}

// Gerar uma cor com base no ID do usuário
function getUserColor(userId: string): string {
  const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4']
  const index = userId.charCodeAt(0) % colors.length
  return colors[index]
}

Use no componente desta forma:

function CollaborativeCanvas({ canvasId, currentUser }) {
  const { cursors, sendCursor } = useCursors(canvasId, currentUser)

  const handleMouseMove = (e: React.MouseEvent) => {
    sendCursor(e.clientX, e.clientY)
  }

  return (
    <div className="relative w-full h-full" onMouseMove={handleMouseMove}>
      {/* Mostrar o cursor dos outros usuários */}
      {Object.entries(cursors).map(([userId, cursor]) => (
        <div
          key={userId}
          className="absolute pointer-events-none"
          style={{ left: cursor.x, top: cursor.y }}
        >
          <div className="w-4 h-4 rounded-full" style={{ backgroundColor: cursor.color }} />
          <span className="text-xs ml-1">{cursor.username}</span>
        </div>
      ))}
    </div>
  )
}

Broadcast vs. Presence

Talvez você tenha percebido que Broadcast e Presence se sobrepõem em alguns pontos. A diferença é:

  • Broadcast serve para envios de alta frequência, que podem ocorrer dezenas de vezes por segundo, como o movimento do cursor e a sincronização de posição em jogos
  • Presence serve para sincronizar estados atualizados ocasionalmente, como “quem está online” e “quem está digitando”

Os dois funcionam melhor em conjunto. Broadcast é indicado para dados em movimento, enquanto Presence é indicado para dados de estado.

Exemplo completo: criando uma aplicação de chat colaborativo

Agora vamos combinar os três modos em uma aplicação de chat realmente colaborativa. Os recursos incluem:

  • Mensagens em tempo real (Postgres Changes)
  • Lista de usuários online (Presence)
  • Indicador de digitação (Presence)

Preparando o banco de dados

Primeiro, crie as tabelas:

-- Tabela de salas
CREATE TABLE rooms (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  name text NOT NULL,
  created_at timestamptz DEFAULT now()
);

-- Tabela de membros das salas
CREATE TABLE room_members (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  room_id uuid REFERENCES rooms(id) ON DELETE CASCADE,
  user_id uuid REFERENCES auth.users(id),
  joined_at timestamptz DEFAULT now(),
  UNIQUE(room_id, user_id)
);

-- Tabela de mensagens
CREATE TABLE messages (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  room_id uuid REFERENCES rooms(id) ON DELETE CASCADE,
  user_id uuid REFERENCES auth.users(id),
  content text NOT NULL,
  created_at timestamptz DEFAULT now()
);

-- Ativar o Realtime
ALTER publication supabase_realtime ADD TABLE messages;

-- Ativar RLS
ALTER TABLE messages ENABLE ROW LEVEL Security;

-- Política RLS: consultar somente as mensagens das salas às quais o usuário pertence
CREATE POLICY "Users can view messages in their rooms"
ON messages FOR SELECT
USING (
  room_id IN (
    SELECT room_id FROM room_members WHERE user_id = auth.uid()
  )
);

-- Política RLS: somente membros da sala podem enviar mensagens
CREATE POLICY "Room members can send messages"
ON messages FOR INSERT
WITH CHECK (
  room_id IN (
    SELECT room_id FROM room_members WHERE user_id = auth.uid()
  )
);

Componente React completo

Este é um componente de chat colaborativo simplificado que reúne os três modos:

import { createClient } from '@supabase/supabase-js'
import { useEffect, useState, useRef, useCallback } from 'react'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

interface Message {
  id: string
  content: string
  user_id: string
  username: string
  created_at: string
}

interface UserPresence {
  user_id: string
  username: string
  is_typing?: boolean
}

export function CollaborativeChat({ roomId, currentUser }) {
  const [messages, setMessages] = useState<Message[]>([])
  const [onlineUsers, setOnlineUsers] = useState<UserPresence[]>([])
  const [typingUsers, setTypingUsers] = useState<string[]>([])
  const [inputValue, setInputValue] = useState('')

  const channelRef = useRef<RealtimeChannel | null>(null)
  const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null)

  useEffect(() => {
    // Criar um channel compartilhado pelos três recursos
    const channel = supabase.channel(`room:${roomId}`, {
      config: { presence: { key: currentUser.id } }
    })
    channelRef.current = channel

    // 1. Postgres Changes: monitorar novas mensagens
    channel.on(
      'postgres_changes',
      {
        event: 'INSERT',
        schema: 'public',
        table: 'messages',
        filter: `room_id=eq.${roomId}`
      },
      (payload) => setMessages(prev => [...prev, payload.new as Message])
    )

    // 2. Presence: monitorar usuários online e estado de digitação
    channel.on('presence', { event: 'sync' }, () => {
      const state = channel.presenceState()
      const users = Object.values(state).flat() as UserPresence[]
      setOnlineUsers(users)

      const typing = users
        .filter(u => u.is_typing && u.user_id !== currentUser.id)
        .map(u => u.username)
      setTypingUsers(typing)
    })

    // Assinar e registrar o Presence
    channel.subscribe(async (status) => {
      if (status === 'SUBSCRIBED') {
        await channel.track({
          user_id: currentUser.id,
          username: currentUser.username,
          is_typing: false
        })

        // Carregar o histórico de mensagens
        const { data } = await supabase
          .from('messages')
          .select('*')
          .eq('room_id', roomId)
          .order('created_at', { ascending: true })

        if (data) setMessages(data)
      }
    })

    return () => supabase.removeChannel(channel)
  }, [roomId, currentUser])

  const sendMessage = async () => {
    if (!inputValue.trim()) return
    await supabase.from('messages').insert({
      room_id: roomId,
      user_id: currentUser.id,
      content: inputValue.trim()
    })
    setInputValue('')
    setTyping(false)
  }

  const setTyping = useCallback((isTyping: boolean) => {
    if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current)
    channelRef.current?.track({
      user_id: currentUser.id,
      username: currentUser.username,
      is_typing
    })
    if (isTyping) {
      typingTimeoutRef.current = setTimeout(() => setTyping(false), 3000)
    }
  }, [currentUser])

  return (
    <div className="flex h-full">
      {/* Lado esquerdo: usuários online */}
      <div className="w-64 border-r p-4">
        <h3 className="font-bold mb-2">Online ({onlineUsers.length})</h3>
        {onlineUsers.map(user => (
          <div key={user.user_id} className="py-1">
            {user.username}
            {user.is_typing && <span className="text-sm text-gray-500"> (digitando)</span>}
          </div>
        ))}
      </div>

      {/* Lado direito: chat */}
      <div className="flex-1 flex flex-col">
        <div className="flex-1 overflow-y-auto p-4">
          {messages.map(msg => (
            <div key={msg.id} className="mb-2">
              <span className="font-bold">{msg.username}: </span>
              {msg.content}
            </div>
          ))}
          {typingUsers.length > 0 && (
            <div className="text-gray-500 text-sm">
              {typingUsers.join(', ')} está digitando...
            </div>
          )}
        </div>

        <div className="p-4 border-t">
          <input
            value={inputValue}
            onChange={e => { setInputValue(e.target.value); setTyping(true) }}
            onKeyDown={e => e.key === 'Enter' && sendMessage()}
            placeholder="Digite uma mensagem..."
            className="w-full p-2 border rounded"
          />
        </div>
      </div>
    </div>
  )
}

Decisões principais de design

  1. Compartilhar um único Channel: os três recursos usam o mesmo channel para reduzir o número de conexões.
  2. Estrutura dos dados do Presence: is_typing fica no mesmo objeto de presence.
  3. Limpeza: sempre chame removeChannel quando o componente for desmontado.

Boas práticas de desempenho e segurança

Até aqui, todo o código está funcionando. Mas ainda há alguns detalhes a resolver antes de colocar a aplicação em produção.

Gerenciamento de conexões

Cada channel ocupa uma conexão WebSocket. Embora o Supabase permita que vários canais compartilhem uma mesma conexão, o uso excessivo ainda pode causar problemas.

Recomendações:

  • Use no máximo dois ou três channels por página
  • Compartilhe um channel entre recursos relacionados, como no exemplo do chat acima
  • Chame removeChannel assim que o usuário sair da página
// Exemplo de limpeza
useEffect(() => {
  const channel = supabase.channel('my-channel')
  channel.subscribe()

  return () => {
    // Não chame apenas unsubscribe; use removeChannel
    supabase.removeChannel(channel)
  }
}, [])

RLS é obrigatório

Não presuma que “filtrar no frontend é suficiente”. As assinaturas do Realtime aplicam automaticamente as políticas RLS, e cada usuário recebe somente as alterações dos dados que tem permissão para consultar.

Se a assinatura não estiver recebendo dados, siga esta ordem de verificação:

  1. A tabela foi adicionada à publication supabase_realtime?
  2. As políticas RLS estão corretas? Use a ferramenta de teste de RLS do Supabase Dashboard.
  3. O usuário está autenticado? O que auth.uid() retorna?

Atenção ao preço

A cobrança do Supabase Realtime é baseada no número de conexões simultâneas: US$ 10 por mil conexões no pico. Para a maioria das aplicações pequenas e médias, a cota gratuita é suficiente. Mas, se muitos usuários ficarem online ao mesmo tempo, controle a quantidade de channels.

O projeto de demonstração oficial Multiplayer.dev mostra os três modos em funcionamento.

Conclusão

Depois de tudo isso, escolher um modo é bem simples:

Precisa armazenar o histórico?Envios de alta frequência?Modo recomendado
SimNãoPostgres Changes
NãoNãoPresence
NãoSimBroadcast

Se você está criando uma aplicação colaborativa, como um chat, quadro branco ou editor de documentos, provavelmente usará os três modos. Postgres Changes armazena as mensagens, Presence acompanha o estado online e Broadcast processa os cursores.

Recomendo começar pelo Multiplayer.dev para ver o funcionamento na prática. Depois, crie um pequeno demo de chat: essa é a forma mais rápida de aprender.

No próximo artigo, pretendo falar sobre Supabase Storage, upload de arquivos e processamento de imagens. Se tiver alguma dúvida sobre Realtime, deixe um comentário.

FAQ

Qual é a diferença entre os três modos do Supabase Realtime?
Cada modo tem uma função:

• Postgres Changes monitora alterações no banco de dados, mantém os dados persistentes e é indicado para mensagens de chat e status de pedidos
• Presence acompanha o estado online dos usuários, mantém os dados em memória e é indicado para listas de pessoas online e indicadores de digitação
• Broadcast transmite mensagens temporárias sem armazená-las e é indicado para rastreamento de cursor e posições em tempo real
Por que minha assinatura não recebe dados?
Verifique três pontos:

1. Se a tabela foi adicionada à publication supabase_realtime (execute ALTER publication supabase_realtime ADD TABLE nome_da_tabela)
2. Se as políticas RLS estão configuradas corretamente (as assinaturas do Realtime aplicam RLS automaticamente)
3. Se o usuário tem permissão para consultar os dados (verifique o valor retornado por auth.uid())
Devo escolher Postgres Changes ou Broadcast?
Escolha Postgres Changes quando precisar armazenar um histórico e consultá-lo depois. Para dados temporários de alta frequência que não precisam de persistência, escolha Broadcast. Por exemplo, use o primeiro para mensagens de chat e o segundo para a posição do cursor.
Os dados do Presence são persistidos?
Não. Os dados do Presence ficam na memória do serviço Realtime e desaparecem quando o usuário se desconecta. Esse modo é adequado para dados temporários, como o estado online.
Como as assinaturas do Realtime são afetadas pelo RLS?
As assinaturas do Realtime aplicam automaticamente as políticas RLS da tabela. Cada usuário recebe somente as alterações dos dados que tem permissão para consultar. Esse é um requisito essencial de segurança em produção; não faça a filtragem no frontend.
Quantos channels uma página pode usar?
Recomendo no máximo dois ou três. Compartilhe um channel entre recursos relacionados, como um chat que usa Postgres Changes e Presence, para reduzir o número de conexões WebSocket. Ao sair da página, sempre chame removeChannel para fazer a limpeza.

16 min de leitura · Publicado em: 15 abr 2026 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog