Entenda Multi-Tenancy na prática (e na teoria)

O que é multi-tenancy, o erro comum de confundir com "sistema com vários usuários", as formas mais populares de identificar um tenant e como estruturar o banco de dados. Com um exemplo prático em Next.js e Prisma.


Multi-tenancy é um termo que sempre existiu, mas ganhou bastante popularidade com a chegada dos SaaS. Hoje em dia é "vamos criar um SaaS para isso", "um SaaS para aquilo", e junto disso vêm técnicas de construção que se repetem entre projetos, sendo o multi-tenancy uma das mais comuns. Vou explicar o conceito na teoria e depois construir um exemplo prático em Next.js para fixar tudo.

O que multi-tenancy não é

Se você olhar para o nome, "tenancy" vem de "tenant", que numa tradução mais grosseira seria "inquilino", como quem mora numa casa. Isso sozinho não explica muita coisa, então vamos direto ao erro mais comum.

Vamos supor que você queira criar um Instagram multi-tenant. Aqui já mora o primeiro erro: achar que multi-tenancy é simplesmente ter vários usuários acessando o mesmo sistema, cada um vendo informações diferentes.

No Instagram normal, se eu acesso com a minha conta, vejo o meu feed. Se acesso com outra conta, vejo outro feed. É a mesma página, o mesmo sistema, só que o conteúdo muda de acordo com quem está logado. Isso não é multi-tenancy, isso é só um sistema com múltiplos usuários, um sistema multiuser.

O que multi-tenancy de fato é

Agora imagina que o Instagram vende para você a possibilidade de criar a sua própria "bolha" dentro do próprio Instagram. A empresa X contrata o Instagram e ganha uma bolha isolada: um Instagram interno só da empresa X, com seus próprios usuários, posts e interações. Quando você acessa pelo link da empresa X, o sistema fica azul, por exemplo.

A empresa Y contrata o mesmo serviço e ganha a sua própria bolha, isolada da bolha da empresa X. Quando acessa pelo link da empresa Y, o sistema fica verde. Os posts da empresa Y não aparecem para a empresa X, e vice-versa. São duas redes sociais diferentes, apesar de ser o mesmo sistema rodando por trás.

É esse detalhe que caracteriza multi-tenancy: não basta ter vários usuários, é preciso ter ambientes isolados, ainda que no código seja um sistema único rodando num servidor só. É como um camaleão que se transforma dependendo de quem (ou de onde) o está acessando.

Como identificar qual tenant está acessando

Para o sistema saber se deve se comportar como "azul" ou "verde", ele precisa primeiro identificar quem está acessando. As formas mais populares:

Domínios ou subdomínios diferentes apontando para o mesmo servidor. clientex.com e clientey.com apontam para o mesmo IP, e o servidor identifica pelo domínio de entrada quem é o tenant.

Um path na própria URL. sistema.com/cliente-1 e sistema.com/cliente-2, ou como subdomínio do mesmo domínio: cliente1.sistema.com.

Um header na requisição. Em uma API, o cliente pode ser identificado por um header customizado ou pelo próprio Authorization, sem que a URL mude.

Não importa qual forma você escolhe, o que importa é que sempre, sempre, sempre existe algum sinal que diz ao sistema qual tenant está acessando. Sem isso, não tem como aplicar nenhuma regra de isolamento depois.

Como estruturar o banco de dados

Aqui mora a dúvida mais comum. Existem duas abordagens populares:

  • Um banco de dados por tenant, todos com a mesma estrutura, mas fisicamente separados.
  • Um banco único, com uma coluna de tenant em cada tabela relevante.

A primeira parece mais segura à primeira vista, já que os dados ficam realmente separados. Um problema no banco de um cliente não afeta os outros. Mas a segunda é a mais usada na prática, porque facilita demais a manutenção: você atualiza o sistema uma vez e todos os tenants recebem a atualização ao mesmo tempo, sem precisar propagar mudanças de schema por dezenas de bancos diferentes.

A desvantagem do banco único é que ele concentra o risco: um bug que esqueça de filtrar pelo tenant, ou um problema no banco, afeta todo mundo de uma vez. Por isso sistemas multi-tenant com banco único dependem muito de backups constantes e de uma regra que precisa ser seguida à risca: toda e qualquer operação de leitura, escrita, atualização ou remoção tem que ser filtrada pelo tenant. Não existe exceção.

Colocando em prática com Next.js e Prisma

Para fixar a teoria, um exemplo simples: um sistema de lista de tarefas multi-tenant, onde cada cliente acessa por um domínio diferente e vê a sua própria lista, com sua própria cor de identidade visual.

Identificando o tenant pelo host

No /etc/hosts (ou equivalente no Windows), é possível criar domínios locais apontando para 127.0.0.1, só para simular múltiplos domínios em desenvolvimento:

127.0.0.1 cliente1.local
127.0.0.1 cliente2.local
127.0.0.1 cliente3.local

No Next.js, dá para capturar o host da requisição com a função headers:

import { headers } from "next/headers";
 
const headersList = await headers();
const host = headersList.get("host");

Essa variável host traz algo como cliente1.local:3000, e dá para remover a porta com um split:

const hostname = host?.split(":")[0];

Modelando tenant e tarefas no Prisma

model Tenant {
  id        Int    @id @default(autoincrement())
  host      String @unique
  name      String
  mainColor String
  tasks     Task[]
}
 
model Task {
  id        Int     @id @default(autoincrement())
  label     String
  completed Boolean @default(false)
  tenantId  Int
  tenant    Tenant  @relation(fields: [tenantId], references: [id])
}

Repare que Task tem um tenantId obrigatório. Toda tarefa pertence a um tenant, sem exceção.

Buscando o tenant pelo host

export async function getTenant(host: string) {
  return prisma.tenant.findFirst({ where: { host } });
}

Buscando só as tarefas daquele tenant

export async function getTasks(tenantId: number) {
  return prisma.task.findMany({ where: { tenantId } });
}

Esse é o ponto mais importante do exemplo inteiro: getTasks recebe um tenantId e filtra por ele. Se essa função esquecesse o where, todas as tarefas de todos os clientes apareceriam juntas, misturadas, para qualquer um que acessasse o sistema. É exatamente esse tipo de descuido que transforma multi-tenancy numa vulnerabilidade real.

Juntando tudo na página

const host = headersList.get("host")?.split(":")[0];
const tenant = host ? await getTenant(host) : null;
 
if (!tenant) return null;
 
const tasks = await getTasks(tenant.id);

Acessando por cliente1.local:3000, o sistema busca o tenant com aquele host, pega só as tarefas ligadas a ele e pinta a tela com a cor cadastrada para aquele cliente. Acessando por cliente2.local:3000, é o mesmo sistema, o mesmo servidor, o mesmo código, mas o resultado é completamente diferente: outra cor, outra lista de tarefas.

Resumindo

Um sistema multi-tenant precisa de duas coisas, sempre:

  1. Uma forma de identificar quem está acessando, seja por domínio, path na URL ou header.
  2. Uma filtragem rigorosa por tenant em toda operação de banco de dados, para que os dados de um cliente jamais vazem para outro.

Se você optar por bancos de dados separados por tenant, o cuidado se concentra na hora de escolher a conexão certa. Se optar por um banco único com uma coluna de tenant, o cuidado se espalha por todo o código, em cada query. É sempre uma troca entre segurança, complexidade de manutenção e velocidade para evoluir o sistema, e cabe a você decidir qual pesa mais para o seu caso.