Matriz de anonimização

O que é apagado, anonimizado ou retido quando um titular exclui a conta — a referência canônica para manter lgpd_erase_user() correta.

Referência canônica do comportamento de lgpd_erase_user() (db/supabase/migrations/048_lgpd_erasure_rpc.sql).

Existe porque a exclusão de conta é o ponto onde um esquecimento vira infração: uma tabela nova que ninguém adicionou à RPC continua guardando dado pessoal de alguém que pediu para ser esquecido — e ninguém percebe, porque nada quebra.

Legenda

  • ELIMINAR — a linha some.
  • ANONIMIZAR — a linha fica, sem identificação do titular.
  • RETER — a linha fica intacta, por obrigação legal.

Matriz

Tabela / recursoAçãoComo
profilesELIMINARDELETE; carrega também o carrinho (cart)
user_addressesELIMINARCascata de profiles
asset_collections, collection_itemsELIMINARCascata
personalized_productsELIMINARDELETE explícito
user_assetsELIMINARDELETE explícito (não confia na FK da 005)
ai_conversationsELIMINARDELETE; cascateia ai_messages
ai_messagesELIMINARCascata da conversa (não tem user_id próprio)
ai_generated_imagesELIMINARDELETE
quote_requestsELIMINARDELETE
vouchersELIMINARDELETE
user_consentsELIMINARCascata de auth.users
ai_chat_usageELIMINARDELETE por identifier = 'user:<uuid>' — não há FK
ordersANONIMIZARLimpa shipping_address, payment_metadata.customer, notes; mantém valores e status
orders.items (JSONB)ANONIMIZARItera o array removendo nota pessoal e URLs de arte; não existe tabela order_items
page_viewsANONIMIZARuser_id, session_id, city, region, timezoneNULL
ai_request_logsANONIMIZARuser_id, session_idNULL
order_status_historyRETERNão contém dado pessoal
fiscal_documentsRETERXML assinado; alterá-lo seria fraude fiscal. Expira em 5 anos
data_requestsRETERuser_idNULL; é a prova de que o pedido foi atendido
Storage — prefixos do titularELIMINARRemovidos antes do banco, senão os caminhos se perdem
auth.usersELIMINARauth.admin.deleteUser(), depois da RPC

Ordem de execução (não é arbitrária)

  1. Storage primeiro. Os caminhos dos arquivos vivem nas linhas do banco. Apagar as linhas antes deixaria objetos órfãos e irrecuperáveis.
  2. RPC no banco. Atômica: ou tudo, ou nada.
  3. auth.users por último. Se algo acima falhar, a conta continua existindo e a operação é retentável. O inverso deixaria um usuário sem dados, no pior estado possível.

Armadilhas conhecidas

ai_chat_usage não tem chave estrangeira. A tabela usa identifier textual (user:<uuid> ou guest:<uuid>), criada assim na migration 019. Sem o DELETE explícito por string, cada conta excluída deixa uma linha órfã guardando o uuid de uma conta que não existe mais. A rotina de retenção (050) também varre esses órfãos, como rede de segurança.

A FK de user_assets pode não existir. A migration 005 a cria dentro de um bloco DO $$ … EXCEPTION WHEN others THEN null, que engole falhas silenciosamente. A 047 verifica e recria se preciso; a RPC não depende disso e faz DELETE direto.

Itens de pedido são JSONB. Quem procurar uma tabela order_items não vai achar. A anonimização precisa iterar orders.items com jsonb_agg.

orders.user_id é ON DELETE SET NULL. O vínculo some sozinho quando auth.users é apagado — mas isso acontece depois da RPC. É justamente esse vínculo ainda vivo que permite a RPC localizar as linhas a anonimizar. Inverter a ordem quebraria a anonimização sem erro nenhum.

Como validar

Com uma conta descartável, criar pedido, conversa de IA, imagem gerada e orçamento; então excluir a conta e conferir:

-- deve retornar 0 linhas
select count(*) from profiles where id = '<uuid>';
select count(*) from ai_messages m
  join ai_conversations c on c.id = m.conversation_id where c.user_id = '<uuid>';
select count(*) from ai_chat_usage where identifier = 'user:<uuid>';

-- deve retornar a linha, sem PII e com o valor intacto
select total, shipping_address, payment_metadata->'customer' from orders
 where id = '<order_id>';

-- deve continuar intacto
select xml_content is not null from fiscal_documents where order_id = '<order_id>';

E abrir /admin/pedidos/<id>: deve renderizar sem erro, exibindo “Removido a pedido do titular (LGPD)” no lugar dos dados pessoais.