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

DadoPrazoExecutado por
page_views14 meseslgpd_apply_retention()
ai_request_logs12 meseslgpd_apply_retention()
ai_chat_usage90 dias (+ órfãos)lgpd_apply_retention()
quote_requests sem conversão24 meseslgpd_apply_retention()
profiles.cart inativo12 meseslgpd_apply_retention()
orders (PII)5 anos → anonimizaçãolgpd_apply_retention()
fiscal_documents.xml_content5 anos → anuladolgpd_apply_retention()
data_requests concluídos5 anoslgpd_apply_retention()
Storage de conta excluídaimediato/api/user/delete
Storage órfãosemanal/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 com public = true serve URL de objeto sem consultar RLS — a policy governa apenas listagem e API. A única proteção real é o objeto viver em bucket com public = 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.