O Multi-Tenancy no Node.js permite que uma aplicação atenda várias organizações mantendo isolamento de dados, configuração, limites e identidade. Cada organização é um tenant, e todas compartilham parte da infraestrutura sem compartilhar informações indevidamente.
O principal risco não é desempenho, mas vazamento entre tenants. Um filtro ausente, cache mal indexado ou job sem contexto pode entregar dados de outra empresa. Por isso, o tenant precisa ser derivado da identidade autenticada e aplicado em todas as camadas.
Neste guia, você aprenderá modelos de banco, contexto por requisição, PostgreSQL, caches, filas, arquivos, rate limiting, observabilidade, migrações, testes e segurança.
O que é Multi-Tenancy?
Multi-tenancy é uma arquitetura em que uma aplicação serve vários clientes isolados. A documentação Multitenant solutions da Microsoft apresenta modelos e decisões. A documentação de Row Security do PostgreSQL mostra uma camada de isolamento no banco.
Para autorização, consulte JWT Seguro no Node.js. Para contexto assíncrono, veja AsyncLocalStorage no Node.js.
Tenant versus usuário
Um usuário pode pertencer a vários tenants e ter funções diferentes em cada um. Não trate userId como tenant ID.
Identificação do tenant
Opções comuns:
- claim validado no token;
- subdomínio;
- domínio customizado;
- seleção de organização na sessão;
- header interno assinado;
- segmento de URL, quando autorizado.
Não confie no corpo
{
"tenantId": "outra-empresa",
"name": "Documento"
}O cliente não deve escolher livremente o tenant. Derive da sessão e ignore ou valide o valor enviado.
Contexto da requisição
const context = {
requestId,
userId: identity.userId,
tenantId: identity.activeTenantId,
roles: identity.roles
};Propague esse contexto para repositories, logs e chamadas internas.
AsyncLocalStorage
const { AsyncLocalStorage } = require('node:async_hooks');
const storage = new AsyncLocalStorage();
app.use((req, res, next) => {
const context = authenticateAndResolveTenant(req);
storage.run(context, () => next());
});O contexto não substitui parâmetros explícitos em operações críticas, mas ajuda na observabilidade.
Modelo 1: banco compartilhado
Todas as tabelas possuem tenant_id:
CREATE TABLE projects (
tenant_id UUID NOT NULL,
id UUID NOT NULL,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (tenant_id, id)
);É econômico e simples de operar, mas exige filtros corretos.
Chave composta
Use tenant na chave e em foreign keys:
CREATE TABLE tasks (
tenant_id UUID NOT NULL,
id UUID NOT NULL,
project_id UUID NOT NULL,
title TEXT NOT NULL,
PRIMARY KEY (tenant_id, id),
FOREIGN KEY (tenant_id, project_id)
REFERENCES projects (tenant_id, id)
);Isso impede relacionar tarefa de um tenant a projeto de outro.
Unique por tenant
UNIQUE (tenant_id, slug)Um slug pode existir em organizações diferentes.
Índices
Comece índices com tenant quando as queries filtram por ele:
CREATE INDEX idx_projects_tenant_created
ON projects (tenant_id, created_at DESC);Queries sempre filtradas
SELECT id, name
FROM projects
WHERE tenant_id = $1
AND id = $2;Não busque apenas por ID, mesmo que UUID pareça imprevisível.
Repository seguro
class ProjectRepository {
constructor(pool) {
this.pool = pool;
}
async findById({ tenantId, projectId }) {
const result = await this.pool.query(`
SELECT id, name
FROM projects
WHERE tenant_id = $1
AND id = $2
`, [tenantId, projectId]);
return result.rows[0] || null;
}
}Row-Level Security
RLS adiciona proteção no PostgreSQL. O próximo artigo aprofunda o tema.
Modelo 2: schema por tenant
Cada tenant recebe um schema:
tenant_a.projects
tenant_b.projectsO isolamento melhora, mas migrations, conexões e quantidade de objetos ficam mais complexos.
search_path
Alterar search_path por requisição pode ser perigoso com pool. Use transação e SET LOCAL, nomes validados e cleanup garantido.
SQL injection em identificadores
Parâmetros SQL não protegem nomes de schema. Use mapeamento interno e quoting seguro; nunca concatene subdomínio diretamente.
Modelo 3: banco por tenant
Cada organização possui banco próprio. O isolamento e restauração são melhores, mas operações, migrations e pools se multiplicam.
Pool por tenant
Não mantenha milhares de pools abertos. Use cache limitado, expiração e proxy de conexões.
Tenant grande isolado
Uma estratégia híbrida mantém pequenos tenants no banco compartilhado e move clientes grandes para bancos dedicados.
Catálogo de tenants
tenant_id | database_key | region | status | planNão armazene senha em texto. Resolva credenciais por Secret.
Roteamento de banco
const database = await tenantDatabaseResolver.resolve(tenantId);
O resolver deve validar status, região e autorização.
Transações
Uma transação deve permanecer no banco do tenant. Não tente uma transação local entre bancos distintos.
Veja Transações PostgreSQL no Node.js.
Cache
Toda chave precisa incluir tenant:
tenant:${tenantId}:project:${projectId}Uma chave sem tenant pode vazar dados.
Cache de autorização
Inclua tenant, user e versão das permissões. Invalide quando a associação muda.
Cache compartilhado
Defina quotas e evite que um tenant expulse todos os dados dos demais.
Arquivos
Use prefixo por tenant:
tenants/{tenantId}/documents/{documentId}O bucket policy ou URL assinada também deve restringir o prefixo.
Nomes de arquivo
Não use nome enviado pelo usuário como caminho. Gere IDs e preserve o nome apenas como metadado.
Filas
Inclua tenant na mensagem:
{
"messageId": "...",
"tenantId": "...",
"type": "report.generate",
"data": {}
}O consumidor valida o tenant antes de acessar dados.
Contexto não deve vazar entre jobs
Crie um novo escopo por mensagem. Não reutilize AsyncLocalStorage de outro job.
Prioridade e fairness
Um tenant com milhões de jobs pode atrasar todos. Use quotas, filas separadas ou scheduling justo.
Rate limiting
Limite por tenant e usuário. Consulte Rate Limiting no Node.js.
Quotas
Planos podem limitar:
- usuários;
- armazenamento;
- requisições;
- jobs;
- projetos;
- exportações;
- retenção.
Quota atômica
Não conte e depois insira sem proteção. Use transação, constraint ou update condicional.
Feature flags
Uma flag pode variar por tenant, mas autorização e plano ainda precisam ser validados.
Veja Feature Flags no Node.js.
Configuração por tenant
Valide schema, defaults e tamanho. Não permita configuração arbitrária executar código ou selecionar URL interna.
Custom domains
Mapeie domínio para tenant após validar Host. Proteja contra host header injection.
Subdomínio
acme.example.com pode resolver tenant, mas o token precisa confirmar que o usuário pertence à organização.
Troca de tenant
Ao trocar organização ativa, gere nova sessão ou atualize contexto autenticado. Não aceite apenas um header frontend.
Impersonação
Suporte administrativo precisa de consentimento, justificativa, tempo limitado e auditoria. Nunca use senha do cliente.
Autorização por recurso
O tenant limita o conjunto, mas o usuário ainda pode não acessar todos os projetos.
Jobs globais
Um job administrativo que percorre tenants deve processar em lotes e definir contexto explicitamente para cada um.
Noisy neighbor
Um tenant pode saturar CPU, banco ou filas. Monitore consumo e aplique limites.
Connection pool
Conexões são recurso compartilhado. Limite concorrência por tenant para evitar monopolização.
Query pesada
Relatórios grandes devem ter timeout, fila e quota. Não execute sincronicamente na API.
Particionamento
Tabelas enormes podem ser particionadas por hash do tenant ou tempo. Avalie índices e manutenção.
Região de dados
Alguns tenants exigem residência regional. O catálogo pode direcionar banco e storage para a região apropriada.
Criptografia
Chaves por tenant podem reduzir impacto de comprometimento e permitir rotação independente.
Segredos por tenant
Integrações externas podem ter credenciais próprias. Armazene em gerenciador de segredos e não em JSON comum.
Migrations compartilhadas
No banco compartilhado, uma migration afeta todos. Use expand-and-contract.
Consulte Migrações de Banco no Node.js.
Migrations por schema ou banco
Execute com controle de concorrência, checkpoints e métricas. Não tente migrar milhares de tenants ao mesmo tempo.
Versões diferentes
Evite manter cada tenant em um schema de versão diferente por muito tempo. A matriz de compatibilidade cresce rapidamente.
Onboarding
- Criar tenant.
- Provisionar recursos.
- Aplicar configuração.
- Criar administrador.
- Validar quotas.
- Registrar auditoria.
- Ativar acesso.
Provisionamento idempotente
Retries não devem criar bancos, buckets ou usuários duplicados.
Offboarding
Desative acesso, exporte quando necessário, aplique retenção e exclua dados com processo auditado.
Backup e restore
No banco compartilhado, restaurar um tenant isolado é difícil. Prepare exportação lógica e ferramentas de recuperação.
Exclusão
Delete por tenant precisa considerar tabelas, arquivos, cache, filas, backups e analytics.
Observabilidade
Logs devem incluir tenant ID, mas métricas não podem usar milhares de tenants como label em todas as séries.
Cardinalidade
Use métricas agregadas por plano ou região. Para investigação por tenant, use logs ou sistema analítico.
Logs
Inclua tenant, request ID e user ID de forma controlada. Não registre dados de negócio sensíveis.
Consulte Logs com Pino no Node.js.
Tracing
Tenant pode ser atributo do span, respeitando políticas de privacidade e cardinalidade da plataforma.
Métricas
Monitore:
- requisições por plano;
- erros de isolamento;
- queries sem tenant;
- uso de pool;
- jobs por tenant;
- quota excedida;
- latência;
- armazenamento.
Auditoria
Registre alteração de membros, permissões, configurações, exportações e impersonação.
Testes unitários
Repositories devem exigir tenantId no tipo e nos parâmetros.
Teste cruzado
test('tenant A não acessa projeto do tenant B', async () => {
const project = await createProject({ tenantId: tenantB });
const result = await repository.findById({
tenantId: tenantA,
projectId: project.id
});
assert.equal(result, null);
});Teste de cache
Crie IDs iguais em dois tenants e confirme chaves separadas.
Teste de fila
Envie jobs intercalados e confirme que cada um usa o contexto correto.
Teste de pool
Se usar search_path ou variável de sessão, confirme reset após cada transação.
Teste de propriedade
Gere operações aleatórias em vários tenants e verifique que resultados nunca cruzam fronteiras.
Static analysis
Regras de lint podem proibir repositories sem tenant e acesso direto ao pool fora da camada segura.
Erros comuns
- Tenant vindo do corpo: cliente escolhe outra organização.
- Query por ID apenas: dados vazam.
- Cache sem tenant: resposta é compartilhada.
- Foreign key sem tenant: relações cruzadas aparecem.
- Contexto global mutável: requisições se misturam.
- Pool por milhares de tenants: conexões explodem.
- Métrica com tenant label: cardinalidade cresce.
Boas práticas
- Derive tenant da identidade.
- Inclua tenant em todas as chaves.
- Use constraints compostas.
- Adicione RLS quando adequado.
- Separe cache e storage.
- Propague contexto em jobs.
- Aplique quotas.
- Teste isolamento cruzado.
- Audite ações privilegiadas.
- Planeje backup e exclusão.
Conclusão
O Multi-Tenancy no Node.js permite compartilhar infraestrutura mantendo cada organização isolada. O tenant deve fazer parte da identidade, das queries, das chaves de cache, dos arquivos e das mensagens.
A segurança depende de defesa em camadas: filtros explícitos, constraints compostas, RLS, contexto correto e testes cruzados. Com quotas, observabilidade e processos de onboarding e exclusão, a arquitetura escala sem transformar uma economia de infraestrutura em risco de vazamento entre clientes.




