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

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:
| Modo | Onde os dados ficam | Casos de uso típicos | Persistência |
|---|---|---|---|
| Postgres Changes | PostgreSQL | Mensagens de chat, notificações e status de pedidos | Sim |
| Presence | Memória (serviço Realtime) | Usuários online e indicadores de digitação | Não |
| Broadcast | Não armazena (encaminhamento imediato) | Movimento do cursor e posições em tempo real | Nã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:
- 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.
- Parâmetro filter: filtre por um campo do banco para evitar mensagens irrelevantes. A sintaxe é
nome_do_campo=eq.valor. - Limpeza da assinatura: lembre-se de chamar
removeChannelquando 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:
- Se a tabela foi adicionada à publication
supabase_realtime - 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
- Compartilhar um único Channel: os três recursos usam o mesmo channel para reduzir o número de conexões.
- Estrutura dos dados do Presence:
is_typingfica no mesmo objeto de presence. - Limpeza: sempre chame
removeChannelquando 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
removeChannelassim 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:
- A tabela foi adicionada à publication
supabase_realtime? - As políticas RLS estão corretas? Use a ferramenta de teste de RLS do Supabase Dashboard.
- 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 |
|---|---|---|
| Sim | Não | Postgres Changes |
| Não | Não | Presence |
| Não | Sim | Broadcast |
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?
• 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?
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?
Os dados do Presence são persistidos?
Como as assinaturas do Realtime são afetadas pelo RLS?
Quantos channels uma página pode usar?
16 min de leitura · Publicado em: 15 abr 2026 · Atualizado em: 4 set 2026
Supabase na prática
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Supabase Storage na prática: upload de arquivos, controle de acesso e CDN
Configure o Supabase Storage com upload padrão e TUS, políticas RLS, isolamento por usuário, URLs assinadas, Smart CDN e transformação de imagens.
Parte 4 de 7
Próximo
Supabase Storage na prática: upload de arquivos, CDN e controle de acesso
Guia prático completo do Supabase Storage: comparação entre três modelos de controle de acesso, upload em partes com TUS, técnicas de otimização do Smart CDN e análise de preços em relação ao R2 e ao S3. Inclui exemplos em React e soluções para problemas comuns.
Parte 6 de 7



Comentários
Entre com GitHub para comentar