Arquitetura multi-tenant para SaaS: guia completo
Toda decisão difícil em um SaaS começa na mesma pergunta: como separar os dados de um cliente dos dados de outro? A resposta (sua arquitetura multi-tenant) define custo de infraestrutura, esforço de operação, postura de segurança e o teto de escala do produto. Acertar no dia 1 é barato. Corrigir depois de 500 clientes em produção é um dos projetos mais caros e arriscados que uma empresa de software pode encarar.
Este guia destrincha os três modelos clássicos de multi-tenancy, os trade-offs reais de cada um, e como tratar isolamento, billing, observabilidade e performance sem virar refém de decisões precoces.
O que é multi-tenancy, afinal
Em um SaaS multi-tenant, uma única aplicação atende múltiplos clientes (tenants) sobre a mesma base de código e, geralmente, a mesma infraestrutura. Cada tenant enxerga apenas seus próprios dados, usuários e configurações, como se tivesse um sistema só seu.
O oposto é o modelo single-tenant, em que cada cliente roda uma instância isolada da aplicação e do banco. Single-tenant simplifica o isolamento, mas multiplica o custo operacional por cliente: cada deploy, cada migração e cada patch de segurança precisa ser replicado N vezes. Multi-tenancy concentra a operação, e é por isso que praticamente todo SaaS moderno nasce multi-tenant.
A questão central não é se você vai compartilhar recursos, mas em qual camada você desenha a fronteira entre tenants.
Os três modelos de multi-tenancy
1. Banco de dados por tenant (database-per-tenant)
Cada cliente recebe seu próprio banco de dados. A aplicação é compartilhada, mas a camada de dados é fisicamente separada.
- Isolamento: máximo. Um bug de query jamais vaza dados entre tenants, porque eles vivem em bancos diferentes.
- Backup e restore por cliente: trivial, você restaura um banco sem tocar nos demais.
- Custo e operação: o ponto fraco. Centenas ou milhares de bancos viram um pesadelo de provisionamento, monitoramento e migração de schema.
Indicado para poucos tenants de alto valor, com requisitos rígidos de compliance (saúde, finanças, governo) ou contratos que exigem isolamento físico.
2. Schema por tenant (schema-per-tenant)
Um único banco, mas cada tenant tem seu próprio schema (no PostgreSQL, um namespace lógico de tabelas). É um meio-termo: mais isolamento que tabelas compartilhadas, menos overhead que bancos separados.
- Isolamento: bom, no nível lógico. As tabelas de cada tenant são distintas.
- Migração: mais complexa. Alterar o schema significa aplicar a mudança em todos os schemas, um a um.
- Limite prático: o PostgreSQL degrada com dezenas de milhares de schemas e tabelas (catálogo gigante, autovacuum sob pressão). Funciona bem na casa das centenas, não dos milhões.
3. Tabela compartilhada com tenant_id (shared schema / pooled)
Todos os tenants vivem nas mesmas tabelas, e cada linha carrega uma coluna tenant_id que identifica o dono. É o modelo mais comum em SaaS de larga escala.
- Custo e operação: ótimo. Uma migração serve para todos; um único banco para monitorar.
- Escala: suporta milhares ou milhões de tenants sem explosão de objetos no catálogo.
- Risco: o isolamento depende inteiramente da aplicação e de políticas de banco. Esquecer um
WHERE tenant_id = ?em uma query é um vazamento de dados entre clientes. Por isso o RLS (adiante) é praticamente obrigatório aqui.
Comparativo rápido
| Critério | Banco por tenant | Schema por tenant | Tabela compartilhada |
|---|---|---|---|
| Isolamento de dados | Máximo (físico) | Bom (lógico) | Depende de RLS/app |
| Custo por tenant | Alto | Médio | Baixo |
| Facilidade de migração | Baixa | Média | Alta |
| Escala (nº de tenants) | Dezenas/centenas | Centenas/milhares | Milhares/milhões |
| Backup/restore por cliente | Trivial | Médio | Complexo |
| Risco de vazamento entre tenants | Mínimo | Baixo | Alto sem proteção |
Não existe modelo "certo": existe o modelo certo para o seu estágio. Muitos SaaS começam com tabela compartilhada e isolam apenas clientes enterprise específicos em bancos dedicados, num modelo híbrido.
Segurança e isolamento: onde a maioria erra
No modelo de tabela compartilhada, a segurança não pode depender de o desenvolvedor lembrar de filtrar por tenant_id. A defesa precisa estar no banco.
O Row-Level Security (RLS) do PostgreSQL aplica filtros automaticamente a toda query, com base no contexto da sessão. Mesmo que uma query esqueça o filtro, o banco só retorna as linhas do tenant ativo:
-- Habilita RLS na tabela
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
-- Política: cada sessão só enxerga linhas do seu tenant
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.current_tenant')::uuid);
-- A aplicação define o tenant no início de cada requisição/transação
SET app.current_tenant = '8f3b...e21';
-- A partir daqui, qualquer SELECT/UPDATE/DELETE é filtrado pelo banco,
-- mesmo sem WHERE tenant_id explícito.
SELECT * FROM invoices;
Boas práticas complementares de isolamento:
- Defesa em profundidade: RLS no banco e filtro por tenant na camada de aplicação. Uma camada protege a outra.
- Conexão com role restrita: a aplicação nunca deve conectar como superusuário, que ignora RLS.
- Criptografia: dados sensíveis criptografados em repouso (disco, backups) e em trânsito (TLS). Para dados altamente sensíveis, considere chaves de criptografia por tenant.
- Testes automatizados de isolamento: crie testes que tentam, de propósito, ler dados de outro tenant. Eles devem falhar sempre.
Billing e planos
O modelo de cobrança costuma se acoplar à arquitetura. Independentemente do modelo de dados, você precisa de:
- Identidade clara do tenant ligada a uma assinatura (plano, ciclo, status).
- Medição de uso (metering): registros de eventos faturáveis (chamadas de API, armazenamento, assentos de usuário) agregados por tenant.
- Enforcement de limites: o plano define cotas (ex.: 10 mil requisições/mês), e a aplicação precisa aplicá-las graciosamente, sem derrubar o serviço.
Mantenha o metering desacoplado da lógica de produto. Emita eventos de uso e processe-os em um pipeline próprio; assim você troca de provedor de billing (Stripe, etc.) sem reescrever o produto.
Observabilidade por tenant
Em multi-tenancy, "o sistema está lento" é uma frase inútil. Você precisa saber para qual tenant está lento. Instrumente desde cedo:
tenant_idem todo log, métrica e trace. Sem isso, diagnosticar incidentes vira adivinhação.- Dashboards segmentáveis por tenant, para isolar quem está consumindo recursos ou sofrendo erros.
- Alertas com contexto de tenant, para que o on-call saiba imediatamente o raio de impacto.
Essa granularidade também alimenta o billing por uso e a análise de churn: o mesmo dado serve a engenharia, finanças e produto.
Performance e o problema do "noisy neighbor"
Quando tenants compartilham recursos, um cliente abusivo pode degradar a experiência de todos os outros. É o clássico noisy neighbor. Mitigações:
- Rate limiting por tenant, não global. Cada tenant tem seu próprio orçamento de requisições.
- Cotas de recursos (CPU, conexões de banco, jobs em fila) por tenant.
- Filas isoladas ou prioridades distintas para evitar que um lote pesado de um cliente trave o processamento dos demais.
- Índices com
tenant_idà frente. Em tabelas compartilhadas, índices compostos começando portenant_idmantêm as queries rápidas mesmo com bilhões de linhas. - Particionamento por tenant (partitioning) para tabelas muito grandes, isolando fisicamente os dados mais quentes.
Migrações e versionamento
Migração de schema é onde os modelos divergem mais na prática. No modelo de tabela compartilhada, uma migração roda uma vez e vale para todos. Simples, porém arriscado: um erro afeta todos os tenants de uma vez. Adote migrações compatíveis para frente e para trás (expand/contract): adicione colunas novas como opcionais, faça o deploy do código que as usa e só então remova o antigo. Isso permite deploys sem downtime e rollback seguro.
No modelo de banco ou schema por tenant, a migração precisa ser orquestrada em lote, com rastreamento de quais tenants já foram migrados e capacidade de retomar de onde parou em caso de falha.
Erros comuns que cobram caro depois
- Adicionar
tenant_idtarde demais. Retrofitar tenancy em um schema que nasceu single-tenant é doloroso. Modele o tenant desde a primeira tabela. - Confiar só na aplicação para isolar dados. Sem RLS, um único bug vira incidente de vazamento. Coloque a defesa no banco.
- Não instrumentar por tenant. Você só sente a falta quando o incidente já está acontecendo e não dá para segmentar nada.
- Escolher banco por tenant cedo demais. O isolamento máximo seduz, mas o custo operacional pode sufocar uma equipe pequena antes da escala chegar.
- Ignorar o noisy neighbor. Sem limites por tenant, o cliente que mais cresce vira o que mais degrada a base.
Como escolher o seu modelo
Em resumo prático:
- Poucos clientes enterprise, compliance pesado, isolamento físico contratual? Banco por tenant.
- Dezenas a centenas de tenants, com necessidade de isolamento lógico forte? Schema por tenant.
- Escala alta, custo enxuto, milhares de tenants? Tabela compartilhada com
tenant_ide RLS. - Realidade da maioria? Híbrido: a base no modelo compartilhado, com contas enterprise isoladas em bancos dedicados conforme a demanda aparece.
A decisão raramente é permanente, mas mudá-la é caro. Modele o tenant_id desde o início, blinde o banco com RLS e instrumente tudo por tenant, assim você mantém a porta aberta para evoluir o modelo sem reescrever o produto.
Construindo um SaaS multi-tenant? Na ConstructorTI, arquitetura multi-tenant, isolamento e billing entram no blueprint desde o dia 1, não como remendo depois que o primeiro cliente enterprise aperta. Se você quer um produto que escala sem virar dívida técnica, fale com a nossa engenharia e saia da conversa com um diagnóstico técnico do seu modelo de tenancy.