Titular pede exclusão (Art. 18 V LGPD). Você tem 15 dias (Art. 19) para responder. Em arquitetura moderna, "DELETE FROM users" não basta — PII vive em 8 a 20 lugares, e esquecer um deles é violação. Este módulo mapeia onde PII se espalha, padrões para sincronizar deleção, e quando crypto-shredding é a única opção viável.
O mapa de propagação de PII
Pipeline de erasure — orquestração
Handler idempotente — código de referência
import { Kafka } from 'kafkajs';
import { Client as ES } from '@elastic/elasticsearch';
import { destroyKey } from './kms';
import { db } from './db';
type ErasureEvent = { request_id: string; user_id: string; scope: 'full' | 'consented_only' };
export async function handle(event: ErasureEvent) {
const stepKey = `${event.request_id}:postgres`;
// idempotência: se já processou, skip
const alreadyDone = await db.query(
'SELECT 1 FROM erasure_step WHERE step_key = $1 AND status = $2',
[stepKey, 'done']
);
if (alreadyDone.rows.length) return;
await db.tx(async (tx) => {
// 1. Hard delete em tabelas com PII
await tx.query('DELETE FROM user_addresses WHERE user_id = $1', [event.user_id]);
await tx.query('DELETE FROM user_devices WHERE user_id = $1', [event.user_id]);
// 2. Anonimiza transações (retidas por obrigação fiscal)
await tx.query(
`UPDATE transactions SET cardholder_name = NULL, masked_pan = NULL,
email = NULL, ip_address = NULL WHERE user_id = $1`,
[event.user_id]
);
// 3. Tombstone na users (para audit) — mantém id, marca anonymized_at
await tx.query(
`UPDATE users SET full_name = NULL, cpf = NULL, email = NULL, phone = NULL,
anonymized_at = NOW() WHERE id = $1`,
[event.user_id]
);
await tx.query(
`INSERT INTO erasure_step (step_key, request_id, status, finished_at)
VALUES ($1, $2, 'done', NOW())
ON CONFLICT (step_key) DO UPDATE SET status='done', finished_at=NOW()`,
[stepKey, event.request_id]
);
});
}
// ES handler: deleta documentos + invalida cache
export async function handleES(event: ErasureEvent) {
const es = new ES({ node: process.env.ES_URL });
await es.deleteByQuery({
index: ['users-*', 'transactions-*'],
refresh: true,
body: { query: { term: { user_id: event.user_id } } },
});
}
// Backups: destrói chave per-tenant
export async function handleBackupCryptoShred(event: ErasureEvent) {
if (event.scope !== 'full') return;
await destroyKey(`user/${event.user_id}/dek`); // KMS schedule deletion (7-30d window)
}Crypto-shredding — quando não dá para apagar fisicamente
Backup mensal de RDS em snapshot S3 de 5 anos. Cliente pede erasure. Restaurar, deletar, gerar backup novo é inviável (custo, RPO, complexidade). Solução: crypto-shredding — granularidade de chave permite "destruir" o dado destruindo a chave.
Granularidade per-user é cara em KMS (custo por chave). Para volume alto, agrupe em tenants/cohorts e use re-encryption + chave de grupo. Documente no DPIA a granularidade efetiva.
Data warehouse: como deletar em colunar particionado
| Plataforma | Estratégia |
|---|---|
| BigQuery | DML DELETE (cota: 1000/dia por tabela). Para volume: MERGE INTO ... DELETE ou recriação de partição. Time Travel até 7 dias — flush após |
| Snowflake | DELETE FROM eficiente; Time Travel até 90 dias (Enterprise); precisa OFFSET para limpar antes do prazo via "Fail-safe" disable ou re-create |
| Redshift | DELETE + VACUUM. Pode ser caro; alternativa: copy-replace com WHERE |
| Databricks / Delta Lake | DELETE FROM (MERGE-on-read); VACUUM com retention < default para limpar Parquet antigo |
| S3 + Athena (Iceberg) | Iceberg suporta DELETE eficiente; Hive não — exige re-escrever partições |
BigQuery tem documento — referência oficial para GDPR/LGPD.
Kafka / SQS — PII em filas
Mensagens com PII em fila são pesadelo de erasure. Estratégias:
ML training set e modelos treinados
Item mais controverso. Frameworks de machine unlearning:
Papers de referência: Cao & Yang (2015) "Towards Making Systems Forget with Machine Unlearning"; Bourtoule et al. (2021) "Machine Unlearning" (arxiv 1912.03817).
Exceções legais — quando NÃO apagar
Art. 16 LGPD permite a conservação para hipóteses específicas. Não invente exceção — cite a norma.
| Categoria | Base para retenção | Prazo típico |
|---|---|---|
| Logs de acesso a aplicação | Marco Civil (Lei 12.965) Art. 15 | 6 meses |
| Registros fiscais | CTN + Lei 9.430 | 5 anos |
| KYC bancário | Lei 9.613 antibranqueamento; Resolução BCB 4.753 | 10 anos pós-término |
| Documentos contratuais | CC Art. 206 §5º III prescrição | 5 anos pós-término |
| Defesa em processo | Art. 7º VI / Art. 16 III LGPD | Até trânsito em julgado + prescrição |
| Estudo por órgão de pesquisa | Art. 7º IV / Art. 11 II c | Anonimização "sempre que possível" |
Em todas as exceções, a finalidade fica limitada à que sustenta a retenção. Não use dado "retido para fiscal" para marketing. Acesso lógico segregado e logado.
DSR portal — interface mínima
import { NextRequest, NextResponse } from 'next/server';
import { authenticate, verifyStepUp } from '@/lib/auth';
import { db } from '@/lib/db';
import { publishToKafka } from '@/lib/kafka';
export async function POST(req: NextRequest) {
const user = await authenticate(req);
if (!user) return NextResponse.json({ error: 'unauthenticated' }, { status: 401 });
// Step-up MFA para operação destrutiva
const stepUpOk = await verifyStepUp(req, { factor: 'totp_or_webauthn' });
if (!stepUpOk) return NextResponse.json({ error: 'step_up_required' }, { status: 403 });
const { scope = 'full', reason } = await req.json();
const { rows: [request] } = await db.query(
`INSERT INTO erasure_request (user_id, scope, reason, status, requested_at)
VALUES ($1, $2, $3, 'received', NOW()) RETURNING id`,
[user.id, scope, reason]
);
// 1. Soft delete imediato
await db.query('UPDATE users SET deleted_at = NOW() WHERE id = $1', [user.id]);
// 2. Publica evento; downstream consumers processam idempotente
await publishToKafka('user.erased', { request_id: request.id, user_id: user.id, scope });
// Logout imediato; sessão revogada
return NextResponse.json({
request_id: request.id,
eta_days: 14,
notice: 'Sua solicitação foi recebida. Dados serão anonimizados em 15 dias úteis conforme Art. 19 LGPD.',
}, { status: 202 });
}Decisão: hard delete imediato ou janela de undo?
📋 Plataforma e-commerce, suporte recebe casos de erasure por engano (cliente confunde excluir conta com excluir pedido)
LGPD não fixa janela mínima — 15 dias é o prazo de RESPOSTA, não de execução. Janela de undo curta protege titular de erro próprio e protege controlador de re-cadastros caros. Acima de 30 dias começa a virar retenção indevida — documente a janela no DPIA e Política de Privacidade.
Alt: Hard delete instantâneo —
Alt: Janela 90 dias —
Alt: Sem hard delete (soft eterno) —