Pedido de cópia dos dados (LGPD): como gerar um export que a pessoa consegue abrir
Monte um export com o que você guarda sobre a pessoa, em arquivos que ela abre sem programa especial, sem dado de terceiros e entregue por link protegido que expira.
A cliente da loja pediu uma cópia de tudo o que vocês têm sobre ela. Uma semana depois recebeu um arquivo user_48213.json com campos como fk_addr_id, datas em milissegundos e o hash da senha dela no meio. Ela não entendeu nada, mandou de novo para o suporte e agora o pedido tem duas reclamações em vez de uma. A resposta curta: o export precisa ser legível por quem pediu, conter o que vocês guardam sobre a pessoa (cadastro, conteúdo, pedidos, preferências, histórico de consentimento), deixar de fora o que é de outras pessoas ou segredo de segurança, e chegar por um caminho protegido que expira.
Na prática, isso vira um pacote com um resumo em HTML que abre no navegador, planilhas CSV para o que é lista (pedidos, endereços) e um JSON para quem quiser levar os dados a outro serviço. Tudo gerado por um job, sem um desenvolvedor rodando SQL na mão a cada pedido.
Por que o export sai no formato do banco
O primeiro export quase sempre nasce como um SELECT * serializado. Quem escreveu pensou em completude, não em leitura. O resultado carrega nomes de coluna internos, chaves estrangeiras sem significado, códigos de status (st=3) e campos técnicos que ninguém fora do time entende.
O outro problema é o contrário: o SELECT * traz demais. Hash de senha, token de sessão, segredo de autenticação em dois fatores, nota interna do antifraude, mensagem que outra pessoa mandou. Parte disso coloca a própria conta em risco se o arquivo vazar; parte é dado de terceiro, que a pessoa não tem direito de receber só porque aparece na mesma tabela.
A LGPD garante ao titular o acesso aos dados e a portabilidade (artigo 18), e o artigo 19 fala em entregar a confirmação ou o acesso em formato simplificado, de imediato, ou por declaração clara e completa num prazo curto. Confira os detalhes no texto oficial e nas orientações da ANPD. O espírito é claro: a pessoa precisa entender o que vocês sabem sobre ela.
Como montar o export passo a passo
- Inventarie as fontes. Liste cada tabela, bucket e serviço externo que guarda dado da pessoa: cadastro, endereços, pedidos, avaliações escritas por ela, preferências de e-mail, registro de consentimento, tickets de suporte. Se você já fez o mapa da adequação à LGPD, ele é o ponto de partida.
- Defina o que sai de cada fonte. Para cada coluna, decida: entra traduzida, entra como está, ou fica de fora com motivo escrito.
- Traduza para gente. Status vira texto ("Entregue"), data vira data e hora no fuso dela, centavos viram reais formatados, chaves internas somem.
- Corte o que não é dela. Mensagens recebidas de outra pessoa, nome de quem avaliou o produto junto, notas internas que revelem regra antifraude. Na dúvida sobre segredo comercial, leve ao jurídico, não decida no código.
- Gere em formatos comuns. HTML para ler, CSV para listas, JSON para portabilidade, tudo num ZIP.
- Entregue com proteção. Arquivo em armazenamento privado, link que exige login, expira em poucos dias e é registrado quando baixado.
- Registre o atendimento. Data do pedido, data de entrega, quem aprovou, o que foi incluído. Isso é a sua prova de que respondeu.
Exemplo: job em Node.js que gera o pacote
No caso da loja, o job recebe o id da cliente, busca as fontes com consulta parametrizada e grava um ZIP numa pasta privada. Usa mysql2/promise e o pacote archiver:
const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
timezone: 'Z'
});
const STATUS = { 1: 'Aguardando pagamento', 2: 'Pago', 3: 'Entregue', 4: 'Cancelado' };
function csv(linhas, colunas) {
const esc = (v) => '"' + String(v ?? '').replace(/"/g, '""') + '"';
return [colunas.map(esc).join(';')]
.concat(linhas.map((l) => colunas.map((c) => esc(l[c])).join(';')))
.join('\r\n');
}
function html(texto) {
return String(texto ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
}
async function gerarExport(clienteId, pastaSaida) {
const [[cliente]] = await pool.execute(
'SELECT nome, email, telefone, criado_em FROM clientes WHERE id = ?', [clienteId]);
if (!cliente) throw new Error('Cliente não encontrada: ' + clienteId);
const [pedidos] = await pool.execute(
'SELECT numero, status, total_centavos, criado_em FROM pedidos WHERE cliente_id = ? ORDER BY criado_em',
[clienteId]);
const [consentimentos] = await pool.execute(
'SELECT finalidade, aceitou, texto_versao, registrado_em FROM consentimentos WHERE cliente_id = ?',
[clienteId]);
const fmtData = (d) => new Date(d).toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' });
const pedidosLegiveis = pedidos.map((p) => ({
numero: p.numero,
situacao: STATUS[p.status] || 'Outro',
total: (p.total_centavos / 100).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }),
data: fmtData(p.criado_em)
}));
const resumo = '<!doctype html><meta charset="utf-8"><title>Seus dados</title>'
+ '<h1>Dados de ' + html(cliente.nome) + '</h1>'
+ '<p>E-mail: ' + html(cliente.email) + '<br>Telefone: ' + html(cliente.telefone)
+ '<br>Cliente desde: ' + html(fmtData(cliente.criado_em)) + '</p>'
+ '<p>Pedidos: ' + pedidos.length + ' (detalhes em pedidos.csv)</p>';
const destino = path.join(pastaSaida, 'export-' + clienteId + '-' + Date.now() + '.zip');
const saida = fs.createWriteStream(destino);
const arquivo = archiver('zip', { zlib: { level: 9 } });
const terminou = new Promise((resolve, reject) => {
saida.on('close', resolve);
saida.on('error', reject);
arquivo.on('error', reject);
});
arquivo.pipe(saida);
arquivo.append(resumo, { name: 'LEIA-PRIMEIRO.html' });
arquivo.append('\ufeff' + csv(pedidosLegiveis, ['numero', 'situacao', 'total', 'data']), { name: 'pedidos.csv' });
arquivo.append(JSON.stringify({ cliente, pedidos: pedidosLegiveis, consentimentos }, null, 2), { name: 'dados.json' });
await arquivo.finalize();
await terminou;
return destino;
}
module.exports = { gerarExport };
Dois detalhes que evitam reclamação. O \ufeff no começo do CSV é o BOM, que faz o Excel abrir acentos corretamente. O separador ; casa com planilhas configuradas em português. E repare no que a consulta não pede: nada de senha_hash, token_2fa ou score_fraude. Pedir só as colunas certas é mais seguro do que pedir tudo e filtrar depois.
Como entregar sem criar um vazamento novo
O ZIP tem endereço, telefone e histórico de compras. Mandar como anexo de e-mail ou num link público eterno transforma o atendimento num risco. O caminho que funciona: guardar o arquivo em armazenamento privado, avisar por e-mail que a cópia está pronta e exigir login para baixar. O link do e-mail leva à área logada, não ao arquivo.
Defina validade (sete dias é um bom padrão) e apague o arquivo depois disso. A lógica é a mesma do link de redefinição que expira. E inclua o próprio arquivo no seu controle de retenção com data, senão a pasta de exports vira uma segunda base de clientes que ninguém lembra de limpar.
Se a pessoa não tem mais acesso à conta, confirme a identidade antes de entregar por outro canal. Enviar o export para quem só disse "sou eu" é entregar os dados a quem pedir primeiro.
Erros comuns
- Exportar o banco cru. A pessoa recebe códigos e chaves, não entende e reabre o pedido. Você atendeu no papel e não no fato.
- Incluir hash de senha ou segredo de 2FA. Se o arquivo vazar, facilita ataque contra a própria conta dela.
- Levar dado de outra pessoa junto. Mensagens recebidas, nomes de quem dividiu o pedido. Você resolve um pedido e cria um incidente com terceiro.
- Esquecer as fontes fora do banco principal. Ticket de suporte, ferramenta de e-mail, planilha do comercial. O export parece completo e não é.
- Link público sem prazo. Qualquer pessoa que receber o e-mail encaminhado baixa tudo, para sempre.
- Não registrar o atendimento. Quando a ANPD ou a própria pessoa perguntar, você não tem como provar data nem conteúdo.
Checklist
- Existe uma lista escrita das fontes que guardam dado de cliente, incluindo serviços externos.
- Cada coluna exportada tem decisão registrada: entra traduzida, entra como está ou fica de fora.
- O pacote tem um arquivo que abre no navegador sem programa especial.
- Hash de senha, tokens e segredos de autenticação não aparecem em nenhum arquivo.
- Dados de outras pessoas foram removidos ou reduzidos ao mínimo.
- O download exige login, expira e o arquivo é apagado depois.
- Cada pedido fica registrado com data de entrada, data de entrega e conteúdo.
- Alguém do suporte consegue disparar o export sem pedir ajuda ao time técnico.
Pegue a conta de teste da sua loja e gere o export agora: você entenderia tudo o que está ali se fosse a cliente, e há alguma coluna no arquivo que não deveria ter saído do banco?
Referência: https://www.gov.br/anpd/pt-br