Design de banco de dados no Supabase: tabelas, relacionamentos e RLS

Você está olhando para aquele aviso vermelho no Supabase Dashboard: “RLS not enabled”. Várias perguntas surgem na hora: os dados dos artigos dos usuários podem vazar? Essa chave estrangeira está correta? Como criar, afinal, um relacionamento muitos para muitos?
Quando comecei a usar o Supabase, caí em muitas armadilhas. Esqueci de ativar o RLS depois de criar uma tabela, e qualquer pessoa conseguia ler todos os dados. Modelei chaves estrangeiras de maneira inadequada: o usuário era excluído, mas os artigos continuavam lá. Para relacionamentos muitos para muitos, cheguei até a tentar armazenar os dados em arrays — um desastre completo.
Depois de alguns meses lidando com esses problemas, finalmente entendi os padrões de design de banco de dados no Supabase. Reuni as conclusões neste artigo.
1. Design da estrutura das tabelas: convenções de nomes do PostgreSQL
1.1 Convenção de nomes: snake_case é o caminho certo
O PostgreSQL tem um comportamento peculiar: quando você não usa aspas duplas, ele converte todos os identificadores em letras minúsculas. Quando usa aspas duplas, ele respeita exatamente o que foi escrito.
O que isso significa? Se você adotar camelCase, como em UserProfile, precisará usar aspas duplas em todas as referências. É trabalho demais.
Por isso, a convenção da comunidade PostgreSQL é: snake_case (palavras separadas por sublinhado), nomes de tabelas no plural e nomes de colunas no singular.
-- ✅ Recomendado
CREATE TABLE users (
id UUID PRIMARY KEY,
email TEXT UNIQUE,
created_at TIMESTAMPTZ
);
1.2 Escolha dos tipos de coluna: não fique preso à lógica do MySQL
Erro 1: usar VARCHAR em vez de TEXT
No PostgreSQL, TEXT e VARCHAR têm exatamente o mesmo desempenho. A única diferença é que VARCHAR(n) impõe um limite de tamanho. A menos que esse limite seja realmente necessário, use TEXT diretamente.
Erro 2: usar TIMESTAMP em vez de TIMESTAMPTZ
TIMESTAMP não armazena informações de fuso horário. Se o servidor estiver nos Estados Unidos e o usuário na China, os horários exibidos ficarão incorretos. TIMESTAMPTZ faz a conversão de fuso horário automaticamente.
Erro 3: usar SERIAL em vez de UUID
SERIAL é um número inteiro autoincremental. Funciona bem em uma aplicação executada em uma única máquina, mas pode causar conflitos em sistemas distribuídos. UUID é globalmente único.
2. Três tipos de relacionamento entre tabelas: um para um, um para muitos e muitos para muitos
2.1 Um para um: basta adicionar UNIQUE
O cenário mais comum é o relacionamento entre usuários e perfis.
CREATE TABLE profiles (
id UUID PRIMARY KEY,
user_id UUID UNIQUE REFERENCES users(id) ON DELETE CASCADE,
bio TEXT
);
O ponto principal é user_id UUID UNIQUE. A restrição UNIQUE garante que cada usuário possa ter apenas um perfil.
2.2 Um para muitos: uma chave estrangeira comum
Pense em autores e livros. Um autor pode escrever muitos livros.
CREATE TABLE books (
id UUID PRIMARY KEY,
author_id UUID REFERENCES authors(id) ON DELETE CASCADE,
title TEXT
);
Na consulta, o Supabase JS permite buscar diretamente os dados relacionados de forma aninhada.
2.3 Muitos para muitos: a tabela de associação é essencial
Pense em estudantes e cursos. Um estudante pode se matricular em vários cursos, e um curso pode ter vários estudantes.
A solução é criar uma tabela de associação.
CREATE TABLE enrollments (
student_id UUID REFERENCES students(id) ON DELETE CASCADE,
course_id UUID REFERENCES courses(id) ON DELETE CASCADE,
PRIMARY KEY (student_id, course_id)
);
3. Row Level Security: o próprio banco de dados controla o acesso
3.1 A filosofia de negar por padrão do RLS
Na primeira vez que usei o Supabase, criei uma tabela posts e fiz uma consulta diretamente pelo frontend com a anon key. O resultado: todos os dados foram retornados. Foi um susto.
O motivo é que o Supabase não ativa a Row Level Security (RLS) por padrão. Sem ela, qualquer pessoa com a anon key pode ler e gravar todos os dados.
Portanto, a primeira regra essencial é: ative o RLS imediatamente depois de criar uma tabela.
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
E depois de ativar o RLS? Ainda não acabou. Ativar o recurso sem criar uma política equivale a negar todo o acesso. É preciso definir pelo menos uma política.
3.2 Sintaxe das políticas: USING e WITH CHECK
- USING: filtra linhas existentes (SELECT, UPDATE e DELETE)
- WITH CHECK: valida novas linhas (INSERT e UPDATE)
3.3 Quatro padrões comuns de políticas
Padrão 1: o usuário acessa os próprios dados
CREATE POLICY "Users manage own data"
ON posts FOR ALL
TO authenticated
USING (user_id = auth.uid());
Padrão 2: combinação de dados públicos e privados
Os itens publicados ficam visíveis para todos, enquanto os rascunhos ficam visíveis apenas para o autor.
Padrão 3: isolamento entre vários tenants
Os membros de uma equipe só podem acessar os dados da própria equipe.
Padrão 4: controle de acesso baseado em funções (RBAC)
Os administradores têm permissões especiais.
4. Otimização de desempenho do RLS
4.1 O vilão do desempenho: executar uma subconsulta para cada linha
Uma subconsulta em uma política de RLS é executada para cada linha de dados. Com 100 mil linhas e uma subconsulta na política para verificar a relação com a equipe, a consulta atingiu um timeout de três minutos.
4.2 Primeira otimização: adicionar índices
As colunas usadas nas políticas de RLS precisam de índices.
CREATE INDEX idx_posts_user_id ON posts(user_id);
Em um teste oficial do Supabase, o tempo foi de 450 ms sem índice e 45 ms com índice: uma melhoria de 10 vezes.
4.3 Segunda otimização: funções SECURITY DEFINER
Coloque a subconsulta em uma função para que ela seja executada apenas uma vez.
CREATE OR REPLACE FUNCTION user_teams()
RETURNS SETOF UUID
LANGUAGE SQL SECURITY DEFINER STABLE
AS $$ SELECT team_id FROM team_members WHERE user_id = auth.uid(); $$;
5. Casos práticos
5.1 Sistema de blog: artigos, categorias e tags
A implementação completa inclui a estrutura das tabelas, as políticas de RLS e a configuração dos índices.
5.2 SaaS multi-tenant: colaboração em equipe
Isolamento dos dados da equipe, acesso dos membros e controle de permissões administrativas.
Conclusão
Pontos principais:
- nomes em snake_case
- chaves primárias UUID, texto em TEXT e datas e horas em TIMESTAMPTZ
- ativação obrigatória do RLS
- otimização com índices e funções SECURITY DEFINER
FAQ
É preciso ativar o RLS logo após criar uma tabela?
Por que o PostgreSQL recomenda snake_case?
Por que não usar FOR ALL nas políticas de RLS?
Como verificar o status do RLS?
6 min de leitura · Publicado em: 4 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 para iniciantes: backend completo com PostgreSQL, Auth e Storage
O Supabase é uma alternativa open source ao Firebase que oferece banco de dados PostgreSQL, autenticação de nível empresarial, armazenamento de objetos e sincronização em tempo real. Este tutorial apresenta os três recursos centrais com exemplos completos e um caso prático para quem desenvolve no frontend e quer criar aplicações full stack rapidamente.
Parte 1 de 7
Próximo
Supabase Auth na prática: verificação de e-mail, OAuth e gerenciamento de sessões
Configure o Supabase Auth com verificação de e-mail, OAuth, sessões JWT e fluxo PKCE para criar uma autenticação segura em aplicações web e SSR.
Parte 3 de 7



Comentários
Entre com GitHub para comentar