Capítulo 30 — Gestão Multi-Tenant
Este capítulo detalha a arquitetura de gestão multi-tenant da Plataforma de Relacionamento Digital com o Cidadão, descrevendo como múltiplos órgãos públicos (tenants) compartilham a mesma infraestrutura e codebase sem co…
30.1 Objetivo do Capítulo
Este capítulo detalha a arquitetura de gestão multi-tenant da Plataforma de Relacionamento Digital com o Cidadão, descrevendo como múltiplos órgãos públicos (tenants) compartilham a mesma infraestrutura e codebase sem comprometer o isolamento de dados, o cumprimento de conformidades específicas ou a personalização de cada organização.
O modelo multi-tenant é uma das características diferenciadores da plataforma. Permite que a PRODEMGE e seus parceiros escalem a solução para dezenas de órgãos (secretarias, prefeituras, autarquias) mantendo um custo operacional controlado, enquanto cada tenant experimenta a plataforma como se fosse uma instância dedicada sob seu controle de governança.
Este capítulo descreve o modelo de segregação lógica, os mecanismos de isolamento de contexto, a personalização por tenant, a governança de dados, a conformidade com LGPD tenant-aware e as decisões arquiteturais que permitem que o sistema escale de um para cem tenants sem degradação de segurança ou desempenho.
30.2 Contexto e Motivação
30.2.1 Cenário Sem Multi-Tenant
Historicamente, a oportunidade de negócio foi concebida com cada órgão operando em instância separada: uma secretaria = um banco de dados = um cluster Kubernetes = uma fatura de infraestrutura. Esse modelo oferecia máxima segurança por isolamento físico, mas criava ineficiências operacionais:
- duplicação de código e configuração em múltiplas instâncias;
- custo infraestrutural proporcional ao número de tenants (não escalável);
- impossibilidade de aproveitar economia de escala;
- dificuldade de orquestração de atualizações (cada instância atualiza em seu ritmo).
30.2.2 Oportunidade Multi-Tenant
A opção por multi-tenant com segregação lógica reconhece que o valor estratégico está em escala: uma plataforma que atende a 50 órgãos simultaneamente com custo controlado é mais viável economicamente do que 50 instâncias isoladas.
A segregação lógica garante que o isolamento é tão robusto quanto o isolamento físico — do ponto de vista de segurança e dados — mas permite compartilhamento de infraestrutura, versionamento unificado, operação centralizada e custos proporcionais ao valor gerado, não ao número de tenants.
30.2.3 Confirmação Regulatória
Os esclarecimentos da PRODEMGE (Montreal, Valtech) confirmam que instâncias lógicas segregadas atendem ao requisito de multi-tenancy do edital. Não é exigida instância física separada por tenant.
30.3 Definições Fundamentais
30.3.1 O que é um Tenant
Um tenant é uma unidade organizacional isolada logicamente que representa uma organização (órgão público, secretaria, autarquia) com:
- identidade única e imutável;
- namespace próprio para dados;
- configurações específicas (políticas de senha, sessão, auditoria);
- usuários e perfis próprios;
- permissões de acesso exclusivas;
- isolamento completo de dados de outros tenants.
Um tenant é a unidade primária de segregação. Toda requisição na plataforma passa pelo Tenant Context — o identificador do tenant resolvido a partir da identidade do usuário autenticado.
30.3.2 Tenant Context
O Tenant Context é a dimensão de segurança que responde a pergunta: "Em qual tenant estou operando nesta requisição?".
O Tenant Context é derivado da identidade autenticada no Capítulo 28. O usuário autenticado possui um ou mais vínculos com tenants (associação de usuário, tenant, perfil, unidade). A escolha de qual tenant opera é feita durante a autenticação ou alternada durante a sessão (com step-up quando requerido).
Uma vez estabelecido o Tenant Context, toda operação subsequente (leitura, escrita, busca, integração) é constrangida por esse contexto — nenhuma operação atravessa a fronteira de tenant.
30.3.3 Segregação Lógica vs. Física
Segregação física: cada tenant em sua própria instância, banco de dados separado, cluster Kubernetes separado, rede separada. Máximo isolamento, custo máximo.
Segregação lógica: todos os tenants em mesma instância, mesma base de dados, mesma rede, mas dados identificados por tenant e isolados em acesso. Custo controlado, isolamento através de controle de acesso.
A plataforma adota segregação lógica. O isolamento é garantido não pela infraestrutura, mas pelos seguintes mecanismos:
- Identificação de todos os dados com tenant_id;
- Filtros obrigatórios em toda query por tenant_id;
- Verificação de Tenant Context antes de toda operação;
- Isolamento de sessões, tokens e cache por tenant;
- Encriptação de dados sensíveis (independente de tenant);
- Auditoria que registra tenant em cada evento.
30.4 Modelo Arquitetural de Multi-Tenant
30.4.1 Camadas de Isolamento
A segregação multi-tenant é implementada em múltiplas camadas:
Camada de Identidade (Cap. 28)
│
└─→ Tenant Context resolvido a partir do usuário autenticado
│
├─ Autorização (Cap. 29): permissões filtradas por tenant
│
├─ Aplicação (Microsserviços)
│ │
│ ├─ Interceptor: Tenant Context injected em todo request
│ ├─ Validação: tenant_id verificado em todo resource ID
│ └─ Query: filtro por tenant_id adicionado automaticamente
│
├─ Persistência (Banco de Dados)
│ │
│ ├─ Schema compartilhado, tabelas com coluna tenant_id
│ ├─ Índices multi-coluna (tenant_id, entity_id, ...)
│ ├─ Triggers que garantem tenant_id em INSERT/UPDATE
│ └─ Row-level security (quando disponível no BD)
│
├─ Cache Distribuído (Redis)
│ │
│ ├─ Chave de cache: "{tenant_id}:{entity_type}:{entity_id}"
│ ├─ Invalidação por tenant quando políticas mudam
│ └─ TTL por tenant_id configurável
│
├─ Busca e Indexação (Elasticsearch)
│ │
│ └─ Índices segregados por tenant (one index per tenant)
│ ou índice compartilhado com field tenant_id e query filter
│
├─ Fila de Mensagens (RabbitMQ)
│ │
│ └─ Topics: "{tenant_id}.{event_type}"
│ ou exchange + routing_key que inclui tenant_id
│
└─ Armazenamento de Objetos (S3/blob storage)
│
└─ Prefixo de chave: "{tenant_id}/{document_uuid}/{file}"
ou bucket por tenant quando necessário
Cada camada implementa sua própria verificação de Tenant Context. Nenhuma camada confia na anterior — todas verificam independentemente.
30.4.2 Identificação de Dados
Toda entidade que contém ou referencia dados de negócio inclui a coluna/campo tenant_id:
User
id (UUID)
tenantId (UUID — qual tenant este usuário pertence)
email
...
Request (Solicitação)
id (UUID)
tenantId (UUID — qual tenant esta solicitação pertence)
citizenId (UUID — cidadão que abriu a solicitação)
status
...
Document (Documento)
id (UUID)
tenantId (UUID)
requestId (UUID)
filePath (string)
...
Interaction (Atendimento)
id (UUID)
tenantId (UUID)
citizenId (UUID)
channel (EMAIL, SMS, WHATSAPP)
...
tenantId é imutável após criação. Não existe "transferência de entidade entre tenants" — isso seria violação de isolamento.
30.5 Ciclo de Vida de um Tenant
30.5.1 Provisioning (Criação)
Quando um novo órgão (tenant) é incorporado à plataforma:
- Pedido de Onboarding: órgão solicita integração (descrito no Capítulo 48);
- Validação: PRODEMGE valida viabilidade legal, conformidade, volume estimado;
- Criação do Tenant:
- Gera UUID único para identificação permanente;
- Registra dados meta (nome, CNPJ, órgão responsável, contato);
- Cria namespace no IAM;
- Inicializa configurações padrão (políticas de senha, timeout de sessão, retenção de dados);
- Ambiente Inicial:
- Cria usuário administrador inicial (Break Glass Account, Capítulo 29);
- Configura domínio/URL para acesso ao portal (ex:
educacao.meu.gov.br); - Ativa integração com Gov.br se requerido;
- Ativação: tenant passa para status ACTIVE e começa aceitar usuários e dados.
30.5.2 Operação (Uso Normal)
Durante a operação:
- Usuários do tenant autenticam e trabalham;
- Dados são criados, modificados, consultados — sempre constrangidos por tenant_id;
- Eventos são registrados com tenant_id na trilha de auditoria;
- Backups incluem tenant_id na partição de backup;
- Relatórios e analytics respeitam fronteira de tenant;
- Integrações com sistemas externos (Gov.br, SEI!MG) passam por camada de adaptação que valida tenant.
30.5.3 Suspensão (Temporária)
Tenant pode ser suspenso por:
- inadimplência (modelo de negócio);
- investigação de segurança;
- requisição do órgão;
- violação de conformidade.
Efeito da suspensão:
- usuários não conseguem autenticar;
- APIs rejeitam requisições;
- operações em background (atualizações, sincronizações) param;
- dados permanecem em banco (preservados para auditoria);
- backups continuam sendo feitos.
Status: SUSPENDED. Pode ser revertido para ACTIVE.
30.5.4 Ofboarding (Exclusão)
Quando um tenant é desativado permanentemente:
- Notificação: órgão recebe aviso prévio (em conformidade com contrato);
- Período de Grace: tempo configurável (ex: 60 dias) para exportação de dados;
- Exportação: órgão pode solicitar dump completo de seus dados;
- Purga de Dados:
- Dados pessoais de cidadãos: anonimizados ou deletados conforme LGPD;
- Dados operacionais (solicitações, documentos): podem ser arquivados ou deletados conforme política de retenção;
- Dados de auditoria: preservados por período legal (LGPD art. 16 — máximo 6 meses por padrão, extensível);
- Status Final: DELETED. Tenant ID preservado em registros históricos, mas inacessível para operações.
30.5.5 Estados do Tenant
┌─────────────┐
│ PENDING │
│ (provisioning)
└──────┬──────┘
│
┌──────▼──────┐
│ ACTIVE │◄──────┐
│ (operando) │ │
└──┬───────┬──┘ │
│ │ reativation
suspend│ │ │
│ activation │
▼ ▼ │
┌──────────────┐ │
│ SUSPENDED │────┘
└──────┬───────┘
│
offboarding
│
┌──────▼───────┐
│ DELETED │
│ (archived) │
└──────────────┘
Transições permitidas:
- PENDING → ACTIVE (ativação bem-sucedida)
- PENDING → DELETED (falha no onboarding)
- ACTIVE → SUSPENDED (suspensão administrativa)
- SUSPENDED → ACTIVE (reativação)
- ACTIVE ou SUSPENDED → DELETED (ofboarding)
30.6 Isolamento de Contexto na Aplicação
30.6.1 Tenant Context Injection
Toda requisição HTTP chega ao backend com credenciais de um usuário autenticado. O primeiro passo após autenticação é derivar o Tenant Context:
HTTP Request
│
├─ Headers: Authorization: Bearer <token>
│
▼
AuthenticationFilter
│
├─ Desserializa token
├─ Valida assinatura
├─ Extrai userId
│
▼
TenantContextFilter
│
├─ Busca userId no IAM
├─ Retorna lista de tenants associados
├─ Se um único tenant: usa automaticamente
├─ Se múltiplos tenants: busca seleção no header (X-Tenant-Id)
├─ Valida que o tenant selecionado é válido para o usuário
│
▼
ThreadLocal<TenantContext>
│
├─ Armazena tenant_id na thread-local
├─ Disponível para toda lógica de negócio
└─ Acessível via TenantContext.getCurrent()
O TenantContext é thread-local (ou request-scoped em frameworks como Spring). Isso garante que:
- Cada requisição tem exatamente um tenant ativo;
- Nenhuma requisição pode "vazar" tenant de outro usuário;
- Múltiplas requisições paralelas de diferentes tenants não se interferem;
- Testes unitários podem usar um tenant de teste sem afetar outros testes.
30.6.2 Validação de Tenant em Operações
Toda operação que toca um recurso identificado valida tenant:
// Exemplo: consultar solicitação
public Request getRequest(UUID requestId) {
UUID currentTenant = TenantContext.getCurrent().getTenantId();
// Busca a solicitação
Request request = requestRepository.findById(requestId);
// Valida que a solicitação pertence ao tenant atual
if (!request.getTenantId().equals(currentTenant)) {
throw new ForbiddenException(
"Solicitação não pertence ao seu tenant"
);
}
return request;
}
Essa validação ocorre em toda operação:
- Leitura: antes de retornar o dado;
- Escrita: antes de permitir modificação;
- Exclusão: antes de permitir deleção;
- Busca: em cada resultado retornado;
- Integração: ao enviar/receber dados de sistemas externos.
Nenhuma operação confia no filtro da query sozinha — a validação é dupla:
- Filtro na query (defesa em profundidade);
- Verificação do resultado contra tenant (segurança explícita).
30.6.3 Propagação para Serviços Internos
A plataforma é baseada em microsserviços. Quando um serviço chama outro serviço internamente, o Tenant Context é propagado:
RequestService
│
├─ currentTenant = TenantContext.getCurrent()
│
└─▶ documentService.uploadDocument(file)
│
├─ Tenant Context propagado via header (X-Tenant-Id: <uuid>)
│
└─▶ DocumentService interceptor
├─ Extrai X-Tenant-Id do header
├─ Valida que matches header Authorization
├─ Estabelece novo TenantContext localmente
└─ Processa upload com tenant context ativo
O header X-Tenant-Id é interno (comunicação entre serviços em private network). Ele não é aceito de requisições do cliente externo — o tenant é sempre derivado da autenticação.
Isso previne que um cliente tente "forjar" acesso a outro tenant passando um header fake.
30.7 Isolamento de Dados na Persistência
30.7.1 Schema do Banco de Dados
O banco de dados compartilhado usa schema único com tenant_id em todas as tabelas:
-- Tabela de usuários
CREATE TABLE public.users (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
password_hash VARCHAR(255),
status VARCHAR(50),
created_at TIMESTAMP,
updated_at TIMESTAMP,
-- Índice composto para queries por tenant
UNIQUE(tenant_id, email),
INDEX idx_users_tenant (tenant_id),
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
-- Tabela de solicitações
CREATE TABLE public.requests (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
citizen_id UUID NOT NULL,
service_id UUID NOT NULL,
status VARCHAR(50),
created_at TIMESTAMP,
updated_at TIMESTAMP,
INDEX idx_requests_tenant (tenant_id),
INDEX idx_requests_citizen_tenant (citizen_id, tenant_id),
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
-- Tabela de documentos
CREATE TABLE public.documents (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
request_id UUID NOT NULL,
file_path VARCHAR(1024),
file_size BIGINT,
content_type VARCHAR(100),
created_at TIMESTAMP,
INDEX idx_documents_tenant (tenant_id),
INDEX idx_documents_request_tenant (request_id, tenant_id),
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
FOREIGN KEY (request_id) REFERENCES requests(id)
);
Princípios:
tenant_idem toda tabela de negócio;- Índices sempre incluem
tenant_id(consultas são sempre filtradas por tenant); - Constraints garantem que chaves estrangeiras respeitem tenant (ex: um usuário de tenant A não pode ter solicitação em tenant B);
- Chaves únicas combinam tenant (ex: email é único por tenant, não globalmente).
30.7.2 Queries Seguras
Toda query produzida pelos microsserviços é interceptada e validada:
// Specification Pattern com verificação de tenant
public class RequestSpecifications {
public static Specification<Request> forTenant(UUID tenantId) {
return (root, query, cb) ->
cb.equal(root.get("tenantId"), tenantId);
}
public static Specification<Request> withStatus(String status) {
return (root, query, cb) ->
cb.equal(root.get("status"), status);
}
// Uso na aplicação
UUID currentTenant = TenantContext.getCurrent().getTenantId();
Specification<Request> spec =
Specification
.where(RequestSpecifications.forTenant(currentTenant))
.and(RequestSpecifications.withStatus("OPEN"));
List<Request> requests = requestRepository.findAll(spec);
}
O padrão garante que:
forTenant(tenantId)é obrigatória em toda query;- Se desenvolvedor esquecer, teste unitário falha (spec incompleta);
- ORM (Hibernate) adiciona filtro tenant automaticamente;
- Query gerada inclui sempre
WHERE tenant_id = ?.
30.7.3 Triggers no Banco de Dados (Camada Defensiva)
Como camada defensiva adicional, triggers garantem integridade de tenant no nível do banco:
-- Trigger: previne INSERT sem tenant_id
CREATE TRIGGER trigger_requests_insert BEFORE INSERT ON public.requests
FOR EACH ROW
BEGIN
IF NEW.tenant_id IS NULL THEN
RAISE EXCEPTION 'tenant_id cannot be NULL';
END IF;
END;
-- Trigger: previne UPDATE que mude tenant_id (imutável)
CREATE TRIGGER trigger_requests_update BEFORE UPDATE ON public.requests
FOR EACH ROW
BEGIN
IF NEW.tenant_id != OLD.tenant_id THEN
RAISE EXCEPTION 'tenant_id is immutable';
END IF;
END;
30.8 Isolamento de Cache Distribuído
30.8.1 Strategy de Cache
O Redis é usado para cache de leitura frequente. O isolamento no cache é feito via chave namespaceada:
Sem isolamento (INSEGURO):
KEY: "user:alice@example.com"
VALUE: {...user_data...}
PROBLEMA: dois tenants com mesmo email conflitam
Com isolamento (SEGURO):
KEY: "tenant:{tenantId}:user:alice@example.com"
VALUE: {...user_data...}
A aplicação nunca forma chave de cache sem incluir tenant:
String cacheKey = String.format(
"tenant:%s:user:%s",
TenantContext.getCurrent().getTenantId(),
userId
);
User cachedUser = redisTemplate.opsForValue().get(cacheKey);
if (cachedUser == null) {
// Busca do banco
User user = userRepository.findById(userId);
// Armazena em cache com tenant
redisTemplate.opsForValue().set(
cacheKey,
user,
Duration.ofHours(1)
);
}
30.8.2 Invalidação de Cache por Tenant
Quando configuração de um tenant muda (ex: política de senha), cache daquele tenant é invalidado:
public void updateTenantPolicy(TenantPolicy policy) {
UUID tenantId = policy.getTenantId();
// Atualiza no banco
tenantPolicyRepository.save(policy);
// Invalida cache do tenant (com padrão)
String pattern = String.format("tenant:%s:*", tenantId);
redisTemplate.delete(
redisTemplate.keys(pattern)
);
}
Isso garante que não há cache "venenado" quando uma mudança é feita.
30.9 Isolamento de Filas e Mensageria
30.9.1 Topics e Routing
RabbitMQ é usado para comunicação assíncrona. O isolamento é feito via topic/routing key que inclui tenant:
Exchange: "platformEvents"
Routing keys por tipo de evento:
- "tenant:{tenantId}.request.created"
- "tenant:{tenantId}.request.approved"
- "tenant:{tenantId}.document.uploaded"
- "tenant:{tenantId}.interaction.closed"
- "tenant:{tenantId}.campaign.executed"
Queue subscription:
Q: "DocumentClassificationWorker_{tenantId}"
Binding: exchange="platformEvents"
routing_key="tenant:{tenantId}.document.*"
Quando um evento é publicado, o publisher inclui tenant:
public void publishRequestCreated(Request request) {
String routingKey = String.format(
"tenant:%s.request.created",
request.getTenantId()
);
rabbitTemplate.convertAndSend(
"platformEvents",
routingKey,
new RequestCreatedEvent(request)
);
}
Subscribers escutam apenas seu tenant:
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(name = "DocumentClassificationWorker_#{tenantId}"),
exchange = @Exchange(name = "platformEvents", type = "topic"),
key = "tenant:#{tenantId}.document.*"
)
)
public void onDocumentEvent(DocumentUploadedEvent event) {
// event.getTenantId() == ThreadLocal tenant automaticamente
documentClassificationService.classify(event);
}
30.9.2 Dead Letter Queues (DLQ) por Tenant
Quando um evento falha, é enviado para DLQ. A DLQ também é segregada por tenant:
Queue: "DocumentClassificationWorker_tenant1_dlq"
Queue: "DocumentClassificationWorker_tenant2_dlq"
Isso previne que falha de um tenant bloqueie fila de outro.
30.10 Isolamento de Busca e Indexação
30.10.1 Elasticsearch: Uma Estratégia
Elasticsearch é usado para busca semântica e full-text (Capítulo 36). Duas estratégias de isolamento:
Estratégia A: Um índice por tenant (recomendado para poucos tenants)
Index: "requests_tenant_<uuid_tenant1>"
Index: "requests_tenant_<uuid_tenant2>"
Index: "requests_tenant_<uuid_tenant3>"
Vantagem: isolamento físico, políticas de retenção por tenant, facilmente deletável. Desvantagem: muitos índices se houver muitos tenants.
Estratégia B: Índice compartilhado com filtro de tenant (recomendado para muitos tenants)
Index: "requests"
Document: {
"id": "req-123",
"tenant_id": "tenant-1",
"content": "Solicitação de habilitação...",
"status": "OPEN"
}
Toda query adiciona filtro de tenant:
SearchRequest searchRequest = new SearchRequest("requests");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// Query do usuário
sourceBuilder.query(
QueryBuilders.boolQuery()
.must(QueryBuilders.multiMatchQuery(
userSearchTerm,
"content", "title"
))
.filter(QueryBuilders.termQuery(
"tenant_id",
currentTenantId.toString()
))
);
searchRequest.source(sourceBuilder);
30.11 Isolamento de Armazenamento de Objetos
30.11.1 Namespace de Prefixos (S3 ou Blob Storage)
Documentos, imagens, anexos são armazenados em objeto storage (S3, Azure Blob, etc.). O isolamento é feito via prefixo:
s3://prodemge-documents/
├─ tenant-1/
│ ├─ request-abc/
│ │ ├─ documento-habilitacao.pdf
│ │ └─ foto-rosto.jpg
│ └─ request-def/
│ └─ comprovante.pdf
│
├─ tenant-2/
│ └─ request-xyz/
│ └─ requerimento.docx
│
└─ tenant-3/
├─ ...
Chave completa: s3://prodemge-documents/{tenant_id}/{entity_type}/{entity_id}/{filename}
Acesso é feito via presigned URL gerada pelo backend:
public String getPresignedDownloadUrl(UUID documentId) {
Document doc = documentRepository.findById(documentId);
// Valida que documento pertence ao tenant
if (!doc.getTenantId().equals(TenantContext.getCurrent().getTenantId())) {
throw new ForbiddenException();
}
// Gera URL assinada (válida por 15 minutos)
String presignedUrl = s3Client.generatePresignedUrl(
bucket,
String.format(
"%s/%s/%s/%s",
doc.getTenantId(),
"requests",
doc.getRequestId(),
doc.getFilename()
),
Duration.ofMinutes(15)
);
return presignedUrl;
}
Políticas de bucket garantem que sem header de autenticação válido (gerado pelo backend), objetos não são acessíveis diretamente — o cliente deve passar pelo backend.
30.12 Isolamento de Auditoria
30.12.1 Trilha de Auditoria Multi-Tenant
Auditoria é registrada em tabela centralizada, mas com tenant_id:
CREATE TABLE audit_log (
id BIGINT PRIMARY KEY,
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
action VARCHAR(50),
resource_type VARCHAR(50),
resource_id UUID,
changes JSONB,
ip_address VARCHAR(45),
user_agent VARCHAR(1024),
timestamp TIMESTAMP,
INDEX idx_audit_tenant (tenant_id),
INDEX idx_audit_tenant_user (tenant_id, user_id),
INDEX idx_audit_tenant_timestamp (tenant_id, timestamp)
);
Toda ação registra tenant_id:
tenant_id: "tenant-1"
timestamp: 2026-07-16 14:32:15
user_id: "user-alice"
action: "REQUEST_APPROVED"
resource_id: "req-12345"
changes: {"status": "OPEN" → "APPROVED", "approved_by": "alice"}
Consultas de auditoria filtram sempre por tenant:
List<AuditLog> getAuditTrail(UUID tenantId, UUID userId) {
return auditRepository.findAll(
Specification
.where((root, query, cb) ->
cb.equal(root.get("tenantId"), tenantId)
)
.and((root, query, cb) ->
cb.equal(root.get("userId"), userId)
)
);
}
30.13 Isolamento de Comunicação e Notificações
30.13.1 Segregação de Canais
Campanhas de comunicação, templates e notificações são segregados por tenant:
CREATE TABLE communication_templates (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
name VARCHAR(255),
channel VARCHAR(50), -- EMAIL, SMS, WHATSAPP
subject VARCHAR(255),
body TEXT,
created_by UUID,
created_at TIMESTAMP,
INDEX idx_templates_tenant (tenant_id),
UNIQUE(tenant_id, name, channel)
);
CREATE TABLE campaigns (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
name VARCHAR(255),
template_id UUID NOT NULL,
segment_id UUID,
status VARCHAR(50),
scheduled_at TIMESTAMP,
executed_at TIMESTAMP,
INDEX idx_campaigns_tenant (tenant_id),
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
FOREIGN KEY (template_id) REFERENCES communication_templates(id)
);
Quando uma campanha é executada, o serviço de comunicação valida tenant:
public void executeCampaign(UUID campaignId) {
Campaign campaign = campaignRepository.findById(campaignId);
// Valida tenant
if (!campaign.getTenantId().equals(TenantContext.getCurrent().getTenantId())) {
throw new ForbiddenException();
}
// Busca template
CommunicationTemplate template =
templateRepository.findById(campaign.getTemplateId());
// Valida que template pertence ao mesmo tenant
if (!template.getTenantId().equals(campaign.getTenantId())) {
throw new DataIntegrityException();
}
// Carrega segmento de cidadãos
List<Citizen> recipients = segmentService.getCitizens(
campaign.getSegmentId(),
campaign.getTenantId()
);
// Enfileira notificações por canal
for (Citizen citizen : recipients) {
notificationQueue.enqueue(
new NotificationTask(
campaign.getTenantId(),
template,
citizen,
campaign.getId()
)
);
}
}
30.13.2 Consentimento Segregado
Consentimentos do cidadão para comunicação são armazenados por tenant:
CREATE TABLE citizen_consents (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
citizen_id UUID NOT NULL,
consent_type VARCHAR(50), -- EMAIL, SMS, WHATSAPP, MARKETING
granted BOOLEAN,
granted_at TIMESTAMP,
revoked_at TIMESTAMP,
INDEX idx_consents_tenant_citizen (tenant_id, citizen_id),
UNIQUE(tenant_id, citizen_id, consent_type)
);
Um cidadão pode ter diferentes consentimentos em diferentes tenants:
Tenant A (Secretaria de Educação):
- citizen-123: EMAIL=true, SMS=false, MARKETING=true
Tenant B (Secretaria de Saúde):
- citizen-123: EMAIL=true, SMS=true, MARKETING=false
Antes de enviar notificação, o serviço valida consentimento para o tenant correto:
boolean canSendEmail(UUID tenantId, UUID citizenId) {
CitizenConsent consent = consentRepository.findOne(
Specification
.where((root, query, cb) ->
cb.and(
cb.equal(root.get("tenantId"), tenantId),
cb.equal(root.get("citizenId"), citizenId),
cb.equal(root.get("consentType"), "EMAIL")
)
)
);
return consent != null && consent.isGranted();
}
30.14 Isolamento de Integrações Externas
30.14.1 Credenciais de Integração por Tenant
Cada tenant pode ter integrações próprias com sistemas externos (SEI!MG, sistemas de trânsito, etc.). Credenciais são armazenadas segregadas:
CREATE TABLE integration_credentials (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
integration_name VARCHAR(100), -- "SEI", "TRAFFIC_SYSTEM", "PROBPMS"
api_key VARCHAR(512), -- encrypted
api_secret VARCHAR(512), -- encrypted
endpoint_url VARCHAR(1024),
status VARCHAR(50),
last_validated_at TIMESTAMP,
INDEX idx_integrations_tenant (tenant_id),
UNIQUE(tenant_id, integration_name)
);
Credenciais são criptografadas em repouso:
public void storeIntegrationCredential(
UUID tenantId,
String integrationName,
String apiKey,
String apiSecret
) {
IntegrationCredential credential = new IntegrationCredential(
tenantId,
integrationName,
encryptionService.encrypt(apiKey),
encryptionService.encrypt(apiSecret)
);
credentialRepository.save(credential);
}
Quando um serviço faz chamada para integração externa, valida tenant e recupera credencial:
public Response callExternalAPI(
String integrationName,
String endpoint,
String payload
) {
UUID currentTenant = TenantContext.getCurrent().getTenantId();
IntegrationCredential credential =
credentialRepository.findOne(
Specification
.where((root, query, cb) ->
cb.and(
cb.equal(root.get("tenantId"), currentTenant),
cb.equal(root.get("integrationName"), integrationName)
)
)
);
if (credential == null) {
throw new NotFoundException("Integration not configured for tenant");
}
String decryptedKey = encryptionService.decrypt(credential.getApiKey());
return externalApiClient.call(
credential.getEndpointUrl(),
endpoint,
decryptedKey,
payload
);
}
30.14.2 Logs de Integração por Tenant
Toda chamada a integração externa é registrada em log segregado:
CREATE TABLE integration_logs (
id BIGINT PRIMARY KEY,
tenant_id UUID NOT NULL,
integration_name VARCHAR(100),
request_at TIMESTAMP,
request_payload TEXT, -- primeiros 4KB por segurança
response_status INT,
response_payload TEXT, -- primeiros 4KB
duration_ms INT,
error_message TEXT,
INDEX idx_integration_logs_tenant (tenant_id),
INDEX idx_integration_logs_tenant_integration (tenant_id, integration_name)
);
Logs podem ser consultados por tenant sem cross-contamination:
List<IntegrationLog> getIntegrationLogs(
UUID tenantId,
String integrationName,
LocalDateTime from,
LocalDateTime to
) {
return logRepository.findAll(
Specification
.where((root, query, cb) ->
cb.and(
cb.equal(root.get("tenantId"), tenantId),
cb.equal(root.get("integrationName"), integrationName),
cb.between(root.get("requestAt"), from, to)
)
),
Sort.by("requestAt").descending()
);
}
30.15 Isolamento na Camada de Negócio
30.15.1 Agregados DDD com Tenant
No DDD (Domain-Driven Design), agregados são construídos com tenant como parte da identidade:
@Entity
@Table(name = "requests")
public class Request {
@EmbeddedId
private RequestId id; // Contém UUID + tenantId
@Embedded
private TenantId tenantId;
@Embedded
private CitizenId citizenId;
@Embedded
private ServiceId serviceId;
private String status;
private LocalDateTime createdAt;
// Constructor valida tenant
public Request(
RequestId id,
TenantId tenantId,
CitizenId citizenId,
ServiceId serviceId
) {
this.id = id;
this.tenantId = tenantId;
this.citizenId = citizenId;
this.serviceId = serviceId;
this.createdAt = LocalDateTime.now();
}
// Métodos de negócio validam tenant implicitamente
public void approve() {
if (!this.status.equals("IN_ANALYSIS")) {
throw new InvalidStateException();
}
this.status = "APPROVED";
}
}
30.15.2 Value Objects com Tenant
Value objects imutáveis encapsulam tenant e previnem comparações cross-tenant:
@Embeddable
public class RequestId implements ValueObject {
@Column(name = "id")
private UUID value;
@Column(name = "tenant_id")
private UUID tenantId;
// Constructor privado
private RequestId() {}
public RequestId(UUID value, UUID tenantId) {
Objects.requireNonNull(value);
Objects.requireNonNull(tenantId);
this.value = value;
this.tenantId = tenantId;
}
// Equals considera tenant
@Override
public boolean equals(Object o) {
if (!(o instanceof RequestId)) return false;
RequestId that = (RequestId) o;
return this.value.equals(that.value) &&
this.tenantId.equals(that.tenantId);
}
@Override
public int hashCode() {
return Objects.hash(value, tenantId);
}
}
30.16 Teste e Validação de Isolamento
30.16.1 Testes Automáticos
Testes de isolamento são obrigatórios antes de merge em main:
@Test
public void testCannotAccessRequestFromOtherTenant() {
// Setup
Tenant tenant1 = tenantRepository.save(new Tenant("Tenant 1"));
Tenant tenant2 = tenantRepository.save(new Tenant("Tenant 2"));
User user1 = createUser(tenant1);
Request request2 = requestRepository.save(
new Request(UUID.randomUUID(), tenant2.getId(), "...")
);
// Attempt
TenantContext.setCurrent(tenant1.getId());
// Assert
assertThrows(
ForbiddenException.class,
() -> requestService.getRequest(request2.getId())
);
}
@Test
public void testCacheRespectsTenantBoundary() {
// Tenant 1 busca usuário
TenantContext.setCurrent(tenant1.getId());
User user1 = userService.getUser(userId);
// Tenant 2 busca usuário com mesmo ID
TenantContext.setCurrent(tenant2.getId());
User user2 = userService.getUser(userId);
// Devem ser usuários diferentes
assertNotEquals(user1, user2);
}
@Test
public void testDatabaseConstraintEnforcesTenant() {
// Tenta inserir documento sem tenant_id
assertThrows(
DataIntegrityViolationException.class,
() -> {
Document doc = new Document();
doc.setId(UUID.randomUUID());
doc.setTenantId(null); // Violação
documentRepository.save(doc);
}
);
}
30.16.2 Verificação de Cross-Tenant Vulnerability
Testes de segurança checam padrões comuns de vulnerabilidade:
@Test
public void testQueryParameterTenantBypassBlocked() {
// Tenant 1 tenta acessar via query param fake
TenantContext.setCurrent(tenant1.getId());
// Mesmo que envie X-Tenant-Id=tenant2, será ignorado
String result = mvc.perform(
get("/api/requests/req-123")
.header("X-Tenant-Id", tenant2.getId().toString())
)
.andExpect(status().isForbidden());
}
@Test
public void testRawSQLInjectionRespectsTenant() {
// Tenta SQL injection para contornar filtro de tenant
TenantContext.setCurrent(tenant1.getId());
assertThrows(
Exception.class,
() -> requestRepository.findAllRaw(
"SELECT * FROM requests WHERE id = '" +
"req-123' OR tenant_id IS NOT NULL --'"
)
);
}
30.17 Riscos e Mitigações
| Risco | Consequência | Mitigação |
|---|---|---|
| TenantContext não inicializado | Operação executa sem tenant, pode vazar dados | Sempre inicializar em filter, falhar se não definido |
| Desenvolvedor esquece de filtrar por tenant | Query retorna dados de outro tenant | Specification obrigatória em repository pattern |
| Cache compartilhado sem namespace | Cidadão acessa dados de outro tenant | Chave de cache sempre inclui tenant_id |
| Trigger de banco não validado | INSERT sem tenant_id não é bloqueado | NOT NULL constraint + trigger BEFORE INSERT |
| JWT do token forjado | Acesso não autorizado a tenant | Sempre validar assinatura de token |
| Rate limiting global sem tenant | Ataque a um tenant degrada outro | Rate limiting por tenant, não global |
| Backup com dados misturados | Restauração contamina tenants | Backup por tenant, segregado fisicamente |
| Log de auditoria centralizado sem tenant | Auditor de um tenant acessa logs de outro | Índice sempre (tenant_id, timestamp), filtro obrigatório |
| Integração externa sem validação de tenant | Credencial de tenant A usada por tenant B | Sempre buscar credencial validando tenant |
| Elasticsearch com query mal formada | Busca vaza dados de outro tenant | Filtro termo de tenant obrigatório em toda query |
30.18 Benefícios do Modelo Multi-Tenant Isolado
- Escalabilidade operacional — novo tenant é onboarded em minutos, não em semanas;
- Custo-benefício — compartilhamento físico de recursos (servidor, banco, cache) reduz custo operacional;
- Isolamento forte — múltiplas camadas de validação (aplicação, banco, cache) garantem segurança;
- Governança por tenant — políticas de senha, sessão, MFA configuráveis independentemente;
- Auditoria desagregada — cada tenant visualiza apenas suas ações;
- Conformidade LGPD — exclusão de dados de um tenant não afeta outro;
- Recuperação granular — restauração de backup de um tenant isolado não impacta outros;
- Equipes operacionais independentes — admin de tenant A não acessa tenant B;
- Modelo econômico flexível — fácil ajustar pricing, features e SLA por tenant;
- Testabilidade — testes de isolamento automatizados garantem integridade contínua.
30.19 Decisões Arquiteturais
| ADR | Tema |
|---|---|
| ADR-101 | Modelo de isolamento lógico vs. físico — segregação lógica em banco único confirmada como suficiente para PRODEMGE |
| ADR-102 | TenantContext em variável ThreadLocal — seguro em ambiente de container com thread pool limitado |
| ADR-103 | Iniciador de TenantContext no HTTP Filter — abordagem recomendada, valida tenant antes de business logic |
| ADR-104 | Specification pattern com Spring Data JPA — garante que nenhuma query escapa do filtro de tenant |
| ADR-105 | Cache distribuído com namespace por tenant — Redis com chave de tenant obrigatória |
| ADR-106 | Políticas de retenção de dados por tenant — cada tenant configura ciclo de vida de seus dados |
| ADR-107 | Índices de banco de dados — (tenant_id, recurso_id) como chave composta primária |
| ADR-108 | Elasticsearch — filtro de tenant em toda query, sem exceção |
| ADR-109 | Arquivos e documentos — armazenamento na nuvem com prefix de tenant_id |
| ADR-110 | Fila de mensagens — RabbitMQ com exchange por tenant, sem mixing de mensagens |
30.20 Considerações Finais
O modelo multi-tenant não é apenas uma característica de escalabilidade — é um compromisso arquitetural fundamental que afeta decisões em cada camada, desde o desenho de tabelas até a forma como logs são consultados.
A força deste modelo reside em três pilares interligados:
-
Defesa em profundidade — não há uma única "barreira de segurança" que, se contornada, exponha dados de outro tenant. A validação ocorre na API, na camada de persistência, no cache, na fila de mensagens, na integração com sistemas externos. Um desenvolvedor que esquece de filtrar por tenant em um lugar não compromete o sistema porque o banco o impede na sequência.
-
Simplicidade operacional — a ausência de infraestrutura separada por tenant significa que onboarding de um novo órgão é uma ação administrativo, não um projeto de meses. Um novo tenant é criado em minutos, com schemas e dados iniciais provisionados automaticamente.
-
Conformidade por design — LGPD exige direito ao esquecimento (exclusão de dados pessoais). Em modelo multi-tenant bem implementado, exclusão dos dados de um tenant é operação isolada, sem risco de afetar outros. Auditoria, backup, recuperação — tudo é desagregável por tenant.
O risco residual é sempre o mesmo em arquiteturas multi-tenant: um desenvolvedor que não compreende a criticidade do isolamento e implementa um atalho. Por isso, o modelo exige:
- Cultura de segurança — toda pessoa na equipe entende que tenant é a primeira questão, não a última;
- Automação de testes — testes de isolamento executam em todo commit, bloqueando merge se falharem;
- Code review orientado a tenant — revisor sempre pergunta: "onde está filtrado por tenant?";
- Documentação periódica — riscos e mitigações são revisados trimestralmente, não apenas durante design inicial.
O Capítulo 31 detalha a implementação do Backend em Java e Spring Boot.
30.21 Controle de Versão
| Campo | Valor |
|---|---|
| Documento | Documento Mestre — Plataforma de Relacionamento Digital com o Cidadão |
| Capítulo | 30 — Gestão Multi-Tenant |
| Versão | 1.0 |
| Situação | Concluído |
| Última atualização | 16/07/2026 |
| Status de Aprovação | Aprovado |
30.22 Rastreabilidade PRODEMGE
Edital CP 001/2026:
- Item 3.1.3 — Arquitetura flexível, multi-tenant, operando em nuvem híbrida, nuvem pública ou on-premise
- Item 3.2.2 — Isolamento lógico de dados por tenant
Plano de Negócio (Anexo I):
- Seção 3.1.2 — Arquitetura modular orientada a serviços
- Seção 3.1.10 — Multi-tenancy com segregação completa
- Seção 3.1.7 — Segurança da informação e isolamento de dados
- Seção 3.3.5 — Conformidade LGPD e proteção de dados pessoais
Funcionalidades (Anexo III) — 60% pontuação:
- Bloco 1 — Integração e Orquestração: Item 1.1 (plataforma integrada com suporte a múltiplos órgãos = multi-tenant)
- Bloco 6 — Segurança, Governança, Acesso: Item 6.2 (controle de acesso com RBAC por tenant), Item 6.4 (configurabilidade por tenant)
Capacidades (Anexo IV) — 30% pontuação:
- Item 3.1.10 — Plataforma multi-tenant com segregação lógica completa de dados
- Item 3.1.6 — Escalabilidade horizontal para suportar múltiplos órgãos em instância única
- Item 3.1.8 — Isolamento de dados e conformidade LGPD por tenant
- Item 3.3.2 — Cache distribuído com segregação por tenant
- Item 3.3.4 — Fila de mensagens com segregação de tópicos por tenant
Sustentabilidade (Anexo V) — 10% pontuação:
- Item 2.2 — Isolamento de dados pessoais conforme LGPD
- Item 2.3 — Proteção de dados pessoais e direitos do titular (exclusão, portabilidade) por tenant
- Item 2.4 — Auditoria desagregada por tenant
Esclarecimentos pertinentes:
- Montreal (02/07/2026), Bloco 1, item 1 — Multi-tenant confirmado como segregação lógica; instâncias lógicas segregadas atendem ao requisito
- Valtech (03/07/2026), item 3.1.10 — Multi-tenancy operando em nuvem é suficiente para pontuação
- Madrona (08/07/2026) — Documentação funcional e de arquitetura aceita como evidência de multi-tenancy
Erratas:
- Errata nº 002 (10/07/2026) — Não aplicável a este capítulo
30.23 Próximo Capítulo
O Capítulo 31 — Backend: Java e Spring Boot detalha a implementação do serviço backend, incluindo arquitetura hexagonal, padrões de design, framework Spring Boot, tratamento de exceções, validação, serialização e integração com os componentes de identidade, autorização, isolamento multi-tenant descritos neste capítulo e no Capítulo 29.
Fim do Capítulo 30 — Gestão Multi-Tenant
Capítulo 29 — Gestão de Perfis e Permissões
Este capítulo detalha o módulo de Gestão de Perfis e Permissões da Plataforma de Relacionamento Digital com o Cidadão, descrevendo como o controle de acesso é modelado, configurado, aplicado e revisado para garantir que…
Capítulo 31 — Backend: Java e Spring Boot
Este capítulo descreve a arquitetura, os padrões, a estrutura de código e as tecnologias do backend da Plataforma de Relacionamento Digital com o Cidadão. O backend é implementado em Java com Spring Boot, seguindo padrõe…