Política de retenção e pendências
Prazo de eliminação de cada conjunto de dados, o job que executa, como verificar se ele está rodando, e o que ainda falta fechar.
Manter dado além da finalidade é infração autônoma (art. 15 e 16), independente de qualquer vazamento. Este documento diz quanto tempo cada coisa vive, quem apaga e como conferir que está apagando.
Prazos
| Dado | Prazo | Executado por |
|---|---|---|
page_views | 14 meses | lgpd_apply_retention() |
ai_request_logs | 12 meses | lgpd_apply_retention() |
ai_chat_usage | 90 dias (+ órfãos) | lgpd_apply_retention() |
quote_requests sem conversão | 24 meses | lgpd_apply_retention() |
profiles.cart inativo | 12 meses | lgpd_apply_retention() |
orders (PII) | 5 anos → anonimização | lgpd_apply_retention() |
fiscal_documents.xml_content | 5 anos → anulado | lgpd_apply_retention() |
data_requests concluídos | 5 anos | lgpd_apply_retention() |
| Storage de conta excluída | imediato | /api/user/delete |
| Storage órfão | semanal | /api/admin/privacy/retention |
Como roda
Agendador: pg_cron dentro do Supabase, semanalmente aos domingos às 04:17
UTC (cron.schedule('lgpd-retention', '17 4 * * 0', …), migration 050).
A função é pura SQL de propósito. Os jobs que falharam no passado neste
projeto (o keep-alive das migrations 013/014) dependiam de pg_net + HTTP
externo. Sem rede e sem token, restam muito menos modos de falha.
Cloudflare Pages não suporta Cron Triggers — só Workers standalone. Por isso
não existe entrada de cron no wrangler.toml e não adianta adicionar.
Como verificar
Cada execução grava uma linha em privacy_job_runs. O painel
/admin/privacidade mostra a última e acusa em vermelho se passar de 10
dias — tolerância de uma execução perdida num job semanal.
Isso existe por uma razão específica: o keep-alive anterior morreu e ninguém percebeu por meses, porque um job silencioso é indistinguível de um job que funciona. A regra aqui é falhar de forma ruidosa.
Verificação manual:
select * from cron.job where jobname = 'lgpd-retention';
select * from privacy_job_runs order by started_at desc limit 5;
select public.lgpd_apply_retention(); -- execução sob demanda
Se o pg_cron estiver fora, o fallback é o endpoint, que roda a mesma RPC e
limpa o Storage órfão (que SQL não alcança):
curl -H "Authorization: Bearer $RETENTION_CRON_TOKEN" \
https://valmor.net.br/api/admin/privacy/retention
Pode ser disparado por qualquer cron externo (cron-job.org, por exemplo) ou pelo próprio administrador logado.
Pendências conhecidas
Registradas aqui porque pendência não documentada vira dívida invisível.
1. Prefixos de Storage ainda públicos
perso-composites/ e uploaded-art/ continuam no bucket público
user-assets. São renderizados em páginas públicas da loja e dentro de pedidos
compartilhados por link, então torná-los privados exige rotear a vitrine inteira
pelo proxy /api/storage/.
Risco: quem descobrir o caminho de um objeto consegue lê-lo.
Não confie em policy para isto. Verificado em produção: com a policy de SELECT do bucket inteiramente removida, um GET anônimo em
/object/public/user-assets/<caminho>continuou retornando o arquivo. Bucket compublic = trueserve URL de objeto sem consultar RLS — a policy governa apenas listagem e API. A única proteção real é o objeto viver em bucket compublic = false.
Foi por isso que quote-designs/, chat-uploads/ e generated-svg/ foram
movidos para o bucket user-private, e não apenas cobertos por policy.
Encaminhamento: avaliar se a arte de personalização precisa mesmo ser pública ou se pode passar pelo proxy com autorização por token do pedido — o mesmo mecanismo aplicado aos anexos de orçamento.
2. Reescrita das URLs persistidas
URLs públicas estão gravadas dentro de linhas do banco
(quote_requests.files[].url, orders.items[].customization.composed_image).
A solução atual converte no momento de exibir, via
src/lib/media/storage-url.ts, o que evita um UPDATE arriscado em JSONB
aninhado e mantém pedidos antigos funcionando.
Consequência: as linhas antigas seguem com string de URL pública, que passou a
ser inválida para prefixos privados. Isso é inofensivo enquanto todo ponto de
exibição usar storageUrlFor() — qualquer novo ponto que use f.url direto
volta a quebrar.
3. E-mails e notificações com imagem
Mensagens enviadas por Resend ou Telegram que embutam imagem de prefixo privado não funcionarão: a rota-proxy exige sessão. Para esses casos, gerar URL assinada de longa duração no momento do envio, ou anexar o arquivo.
Regra de manutenção
Toda tabela nova com dado pessoal precisa de prazo aqui e em
lgpd_apply_retention(). Sem isso, o dado fica para sempre — que é a infração
que esta política existe para evitar.