Checklist de implementação: exclusão de conta e retenção de dados num SaaS multi-tenant

Escrito por

em

Um cliente abre um chamado pedindo a exclusão da conta. O suporte marca deleted_at na tabela accounts, o login para de funcionar e o ticket é fechado. Três semanas depois o mesmo e-mail aparece num relatório de churn, num backup restaurado em staging e num índice de busca que ninguém lembrou de reprocessar. O dado não foi excluído — foi escondido da interface.

Exclusão de conta em SaaS multi-tenant é um problema de inventário, não de comando SQL. Quem trata como DELETE FROM descobre o tamanho real do problema quando a primeira solicitação formal chega com prazo para resposta. O checklist abaixo cobre a implementação em quatro camadas: mapeamento, decisão por tipo de dado, propagação para sistemas satélites e prova de execução.

O que “excluir a conta” significa em cada camada

A confusão começa porque três operações diferentes carregam o mesmo nome. Desativar a conta bloqueia autenticação e mantém tudo. Anonimizar quebra o vínculo entre o registro e a pessoa, preservando o registro. Remover apaga a linha. Cada uma resolve um problema distinto e nenhuma resolve os três.

Na prática, uma exclusão de conta real é uma combinação: alguns dados removidos, outros anonimizados, alguns preservados por obrigação legal ou contratual. Uma nota fiscal emitida não pode ser apagada porque o cliente pediu — mas o nome do usuário que a gerou pode ser substituído por um identificador opaco. Uma linha de log de auditoria que registra “usuário X alterou permissão de Y” perde sentido se o campo virar nulo, mas continua auditável se virar um hash estável.

A decisão precisa ser tomada por categoria de dado, documentada, e refletida no código. Sem isso, cada engenheiro que encosta no fluxo aplica um critério próprio.

Há ainda um efeito colateral típico de multi-tenant: o dado de um tenant referencia o dado de outro. Um usuário convidado que participa de dois workspaces, um recurso compartilhado entre organizações, um comentário feito por alguém de fora. Remover o tenant A sem checar essas bordas quebra a integridade do tenant B — ou, pior, vaza para o B a informação de que o A existiu. A regra prática é tratar identidade de usuário e associação a tenant como entidades separadas: a associação morre com o tenant, a identidade só morre quando não sobra nenhuma associação.

Mapeie o dado antes de escrever o primeiro DELETE

O inventário é a parte cara e a que costuma ser pulada. Num SaaS multi-tenant maduro, o dado de um tenant costuma estar em mais lugares do que o schema sugere:

  • Tabelas com tenant_id explícito — as fáceis, encontráveis por consulta ao catálogo do banco.
  • Tabelas ligadas por chave estrangeira indireta — anexos, comentários e eventos que referenciam um recurso que referencia o tenant.
  • Object storage — uploads, exportações geradas, PDFs, avatares. Costumam estar em prefixos por tenant, mas nem sempre.
  • Índices de busca e caches — Elasticsearch, Redis, cache de aplicação. Não seguem o ciclo de vida do banco.
  • Filas e dead-letter queues — mensagens não processadas carregam payload com dado pessoal.
  • Sistemas de terceiros — provedor de e-mail transacional, ferramenta de suporte, analytics de produto, gateway de pagamento, data warehouse.
  • Logs de aplicação — o lugar onde dado pessoal vaza por acidente com mais frequência.

O mapeamento vira artefato versionado no repositório, não uma planilha. Um arquivo declarativo listando cada destino, a classe de dado que ele guarda e a ação esperada permite escrever um teste que falha quando alguém cria uma tabela nova com tenant_id e esquece de registrá-la.

Uma consulta ao catálogo do Postgres devolve todas as tabelas com coluna de tenant e serve de base para o teste de cobertura do inventário:

SELECT table_name FROM information_schema.columns WHERE column_name IN ('tenant_id','account_id') AND table_schema='public' ORDER BY table_name;

Compare o resultado com o manifesto declarado e falhe o build na diferença.

Soft delete, anonimização ou remoção física: o critério

A escolha depende de duas perguntas: o dado identifica uma pessoa e existe obrigação de retenção sobre ele.

Tipo de dado Ação Por quê
Credenciais, tokens, chaves de API Remoção física imediata Nenhum valor de retenção, risco alto se vazarem
Conteúdo produzido pelo tenant (documentos, registros de negócio) Remoção após janela de arrependimento Pertence ao cliente, sem valor para o provedor
Registros fiscais e contratuais Preservar, anonimizar campos pessoais acessórios Retenção obrigatória por prazo legal
Log de auditoria de segurança Preservar com identificador pseudonimizado Precisa manter rastreabilidade de eventos
Métricas agregadas de uso Preservar se já não forem identificáveis Agregado sem chave de reidentificação deixa de ser dado pessoal
Backups Expirar pelo ciclo normal de retenção Edição cirúrgica de backup quebra a integridade da cópia

A janela de arrependimento merece atenção. Trinta dias entre a solicitação e a remoção definitiva reduz drasticamente o volume de restaurações de emergência, e é curta o bastante para caber num prazo de resposta razoável. O erro comum é implementar a janela e não implementar o job que executa a remoção quando ela termina — a conta fica em pending_deletion para sempre.

Sobre soft delete: ele é uma ferramenta de reversibilidade operacional, não uma forma de exclusão. Se a coluna deleted_at é o único mecanismo, a exclusão não aconteceu. Vale usá-la como estado intermediário e ter um processo separado que consome esse estado.

Backups, réplicas e logs: onde o dado sobrevive à exclusão

Backup é a exceção legítima. Tentar apagar uma linha de dentro de um dump comprimido de dezenas de gigabytes é caro, arriscado e invalida a garantia de restauração. A posição defensável é declarar o prazo de retenção de backup, mantê-lo curto o suficiente para ser justificável, e registrar que dados excluídos persistem nas cópias até expirarem naturalmente — com o compromisso de reaplicar a exclusão caso alguma restauração traga o registro de volta.

Esse compromisso precisa virar código. Um procedimento de restauração que não reexecuta a fila de exclusões pendentes é um procedimento que ressuscita contas removidas. Vale manter uma tabela de exclusões confirmadas fora do ciclo de backup principal, ou pelo menos um log append-only de identificadores excluídos que o pós-restore consulta.

Réplicas de leitura seguem o primário e não exigem tratamento separado. Já um data warehouse alimentado por CDC ou por carga noturna é um sistema independente: precisa receber o evento de exclusão explicitamente. O mesmo vale para o índice de busca, que quase sempre é atualizado por evento e quase nunca tem o evento de exclusão testado.

Logs de aplicação são o ponto cego. Se o pipeline registra corpo de requisição, headers ou payload de erro, há dado pessoal ali. A saída realista não é apagar log — é reduzir a retenção da camada que contém payload e mascarar campos sensíveis na origem, antes de escrever.

Vale medir o custo antes de prometer prazo. Num SaaS com object storage volumoso, a remoção de arquivos de um tenant grande pode levar horas e competir por throughput com o tráfego de produção. Rodar isso de forma síncrona dentro de uma requisição HTTP não funciona; rodar sem limite de taxa degrada o serviço de todo mundo. O job precisa de controle de concorrência e de janela preferencial, e o prazo comunicado ao cliente precisa acomodar o pior caso, não o caso médio.

Checklist de execução

A sequência abaixo assume um SaaS multi-tenant com banco relacional, object storage, índice de busca e integrações de terceiros. Cada item é verificável.

  • Solicitação registrada com identificador do tenant, quem pediu, data e canal. Sem esse registro não há como provar prazo.
  • Autenticação reforçada do pedido — exclusão disparada por e-mail sem confirmação é vetor de ataque. Exija ação autenticada de um administrador do tenant ou confirmação por segundo canal.
  • Conta movida para estado de exclusão pendente, com acesso bloqueado, integrações desativadas e webhooks suspensos.
  • Cancelamento da assinatura no gateway, para não gerar cobrança sobre uma conta em remoção.
  • Job agendado para o fim da janela, com identificador idempotente. Reexecução do job não pode falhar nem duplicar efeitos.
  • Execução por manifesto: o job percorre o inventário declarado, não uma lista hardcoded. Cada destino devolve confirmação.
  • Propagação para terceiros com tratamento de falha explícito — provedor fora do ar não pode fazer o job inteiro abortar sem retomada.
  • Invalidação de cache e reindexação do índice de busca, com verificação de que a consulta pelo tenant devolve vazio.
  • Registro de conclusão em log append-only, fora do ciclo de backup padrão, com carimbo de tempo e lista de destinos confirmados.
  • Verificação automatizada que roda depois do job e consulta cada destino procurando resíduo. Falha aqui abre incidente.

Dos dez itens, os dois que mais faltam em implementações reais são a idempotência do job e a verificação posterior. Sem idempotência, a primeira falha parcial deixa o sistema num estado que ninguém sabe reconciliar. Sem verificação, a equipe descobre o resíduo pela reclamação do cliente.

Comece pelo manifesto e pelo teste de cobertura que compara o manifesto com o catálogo do banco. É o item mais barato de construir e o único que impede a regressão silenciosa quando o schema crescer.

Comentários

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *