Capítulo 17 de 35

Autenticação B2B Multi-Tenant: Isolamento Absoluto de Dados com Supabase RLS

Índice do Manual

O pesadelo número um de qualquer CEO de um sistema SaaS B2B não é o servidor cair. É o dono da Empresa A fazer login no painel e, por um erro no código, enxergar o faturamento e os dados dos clientes da Empresa B.

Na arquitetura clássica, os desenvolvedores tentam impedir isso colocando comandos WHERE company_id = X espalhados por centenas de arquivos na API Node.js. Se um desenvolvedor júnior esquecer de colocar esse WHERE em apenas uma rota, os dados vazam.

Mas a segurança de isolamento de dados não deve morar no código da aplicação; ela deve morar no núcleo do Banco de Dados. E é exatamente isso que o Supabase Row Level Security (RLS) faz.

Este manual serve como a fundação arquitetural para qualquer SaaS Multi-Tenant orquestrado.


1. O Modelo de Dados (A Fundação)

Para que o isolamento funcione de forma impenetrável, precisamos amarrar o usuário que está logado (Autenticação) a uma Empresa (Tenant).

Abaixo está o script SQL padrão de criação que injetamos no Supabase.

-- 1. Criação da tabela de Locatários (Empresas)
CREATE TABLE companies (
  id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
  name TEXT NOT NULL,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- 2. Tabela de Perfis estendendo o auth.users do Supabase
CREATE TABLE profiles (
  id UUID REFERENCES auth.users(id) PRIMARY KEY,
  company_id UUID REFERENCES companies(id) NOT NULL,
  full_name TEXT,
  role TEXT DEFAULT 'member'
);

-- 3. Tabela de negócio comum (Ex: Faturas do sistema)
-- Toda tabela do SaaS OBRIGATORIAMENTE precisa da coluna company_id
CREATE TABLE invoices (
  id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
  company_id UUID REFERENCES companies(id) NOT NULL,
  amount DECIMAL(10,2) NOT NULL,
  client_name TEXT NOT NULL
);

Nesta estrutura, o usuário faz o login, o Supabase identifica o ID dele e, através da tabela profiles, nós sabemos exatamente a qual company_id ele pertence.


2. Ativando a Proteção (RLS)

Com as tabelas criadas, o próximo passo é ativar o campo de força. Sem habilitar o RLS, o banco de dados confia em qualquer requisição que venha da sua API (via Service Key). Nós não queremos isso. Nós queremos que o banco de dados confie apenas no Token (JWT) do usuário logado.

-- Ativando o isolamento de segurança nas tabelas
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;

A partir desse milissegundo, se a sua API ou Frontend tentar fazer um SELECT * FROM invoices, o banco de dados retornará um vazio total. Ninguém consegue ler ou escrever nada até que nós definamos as regras matemáticas.


3. As Políticas de Segurança Invioláveis (Policies)

Este é o coração da arquitetura. Nós vamos escrever uma regra de banco de dados que diz o seguinte: "Um usuário só pode ler ou inserir uma Fatura se o company_id da fatura for exatamente igual ao company_id do perfil dele".

-- 1. Função de alta performance para pegar a Empresa do usuário logado
-- Usamos uma função para cachear a verificação e não engasgar o banco
CREATE OR REPLACE FUNCTION get_user_company_id()
RETURNS UUID AS $$
  SELECT company_id FROM profiles WHERE id = auth.uid();
$$ LANGUAGE sql SECURITY DEFINER;

-- 2. A Política de Segurança da Fatura
CREATE POLICY "Isolamento Total: Usuários só veem faturas da sua empresa"
ON invoices
FOR ALL
USING (
  company_id = get_user_company_id()
);

Por que essa arquitetura é perfeita?

Imagine que um hacker tentou manipular a sua API ou que um estagiário na equipe do frontend esqueceu de colocar filtros e fez uma requisição crua querendo ler o faturamento da concorrência: supabase.from('invoices').select('*').

Mesmo que o código esteja errado ou malicioso, a requisição chega no Postgres e o RLS atua como um juiz final:

  1. "Quem está fazendo essa requisição?" O banco lê o JWT invisível e descobre o ID do usuário.
  2. "Qual a empresa dele?" A função roda e descobre que ele é da Empresa A.
  3. "Toma aqui os dados". O banco joga no lixo todas as linhas da Empresa B e da Empresa C, e devolve apenas as linhas da Empresa A.

A segurança é garantida na camada física do armazenamento.


O Padrão Ouro para B2B

Se o seu sistema atende múltiplas empresas e os dados estão separados apenas por filtros fracos em Javascript na camada de aplicação (Node.js), você está a um bug de distância de um processo judicial milionário por vazamento de dados.

O Supabase e o PostgreSQL não são apenas repositórios de armazenamento; eles são motores de lógica militar de segurança. Centralizar o Multi-Tenant no RLS é o que separa sistemas caseiros de plataformas Enterprise.