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 / recurso | Ação | Como |
|---|---|---|
profiles | ELIMINAR | DELETE; carrega também o carrinho (cart) |
user_addresses | ELIMINAR | Cascata de profiles |
asset_collections, collection_items | ELIMINAR | Cascata |
personalized_products | ELIMINAR | DELETE explícito |
user_assets | ELIMINAR | DELETE explícito (não confia na FK da 005) |
ai_conversations | ELIMINAR | DELETE; cascateia ai_messages |
ai_messages | ELIMINAR | Cascata da conversa (não tem user_id próprio) |
ai_generated_images | ELIMINAR | DELETE |
quote_requests | ELIMINAR | DELETE |
vouchers | ELIMINAR | DELETE |
user_consents | ELIMINAR | Cascata de auth.users |
ai_chat_usage | ELIMINAR | DELETE por identifier = 'user:<uuid>' — não há FK |
orders | ANONIMIZAR | Limpa shipping_address, payment_metadata.customer, notes; mantém valores e status |
orders.items (JSONB) | ANONIMIZAR | Itera o array removendo nota pessoal e URLs de arte; não existe tabela order_items |
page_views | ANONIMIZAR | user_id, session_id, city, region, timezone → NULL |
ai_request_logs | ANONIMIZAR | user_id, session_id → NULL |
order_status_history | RETER | Não contém dado pessoal |
fiscal_documents | RETER | XML assinado; alterá-lo seria fraude fiscal. Expira em 5 anos |
data_requests | RETER | user_id → NULL; é a prova de que o pedido foi atendido |
| Storage — prefixos do titular | ELIMINAR | Removidos antes do banco, senão os caminhos se perdem |
auth.users | ELIMINAR | auth.admin.deleteUser(), depois da RPC |
Ordem de execução (não é arbitrária)
- Storage primeiro. Os caminhos dos arquivos vivem nas linhas do banco. Apagar as linhas antes deixaria objetos órfãos e irrecuperáveis.
- RPC no banco. Atômica: ou tudo, ou nada.
auth.userspor ú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.