Alternar tema

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

Easton editorial illustration: database service control desk

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?
Sim. Essa é uma regra essencial de segurança. O Supabase não ativa o RLS por padrão, então qualquer pessoa com a anon key pode ler e gravar todos os dados.
Por que o PostgreSQL recomenda snake_case?
O PostgreSQL converte identificadores sem aspas em letras minúsculas. Com nomes em camelCase, seria necessário usar aspas duplas em todas as referências.
Por que não usar FOR ALL nas políticas de RLS?
O desempenho de FOR ALL é inferior ao de quatro políticas separadas. Com políticas separadas, o PostgreSQL pode otimizar o uso de índices para cada operação.
Como verificar o status do RLS?
Confira o status do RLS de cada tabela na página de banco de dados do Supabase Dashboard. A cor vermelha indica que ele não está ativado.

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

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog