Popular PostgreSQL com dados brasileiros para staging
Popular PostgreSQL com dados brasileiros para staging
Usar dados de produção em ambiente de staging é um problema de compliance, não de conveniência. A alternativa prática é popular o banco com dados sintéticos válidos — CPFs que passam no mod-11, CNPJs com dígito verificador correto, endereços onde CEP, cidade e estado são coerentes entre si. Este guia mostra como fazer isso do zero com PostgreSQL, um script TypeScript e a API REST do FakeForge BR.
Staging com dados reais é um risco desnecessário
O que a LGPD Art. 7º diz sobre uso de dados pessoais em ambientes de teste
A Lei Geral de Proteção de Dados (LGPD) exige base legal para cada operação de tratamento de dados pessoais. O Art. 7º lista as hipóteses permitidas; nenhuma delas cobre "copiar CPFs reais para o banco de staging porque é mais fácil". O Art. 46 vai além: exige que o controlador adote medidas técnicas para proteger dados pessoais de acessos não autorizados. Um banco de staging acessível por toda a equipe de desenvolvimento, sem as mesmas proteções do banco de produção, viola esse princípio.
A sanção administrativa (Art. 52) pode chegar a 2% do faturamento bruto, limitada a R$ 50 milhões por infração. O risco não compensa.
Por que dados mascarados frequentemente corrompem testes de integração
Mascaramento substitui valores reais por valores aleatórios — um CPF vira uma sequência de 11 dígitos sem validade de checksum. Isso quebra:
- Validações de frontend que conferem o dígito verificador antes de submeter o formulário
- Integrações com gateways de pagamento que rejeitam CPFs inválidos no sandbox
- Testes de fluxo de emissão de nota fiscal onde o CNPJ precisa ser válido
O resultado são falsos negativos nos testes: o código está certo, mas o teste falha por causa do dado.
A alternativa: dados sintéticos válidos que respeitam regras de negócio brasileiras
Dados sintéticos gerados com as mesmas regras do dado real — mod-11 para CPF/CNPJ, algoritmo de Luhn para cartões, correlação CEP × DDD × estado — passam em todas as validações sem expor nenhuma pessoa real. É o melhor dos dois mundos: compliance com a LGPD e testes que refletem o comportamento em produção.
O que faz um dado brasileiro "válido" para testes
Dígitos verificadores: CPF mod-11, CNPJ mod-11, Luhn para cartões
CPF e CNPJ usam o algoritmo mod-11 em dois dígitos verificadores. Um CPF gerado aleatoriamente tem cerca de 1% de chance de ser válido por acaso — suficiente para passar em alguns testes, mas garantido para falhar em outros. Geradores que implementam o cálculo correto eliminam essa variância.
Cartões de crédito usam o algoritmo de Luhn: a soma dos dígitos, com pesos alternados, deve ser divisível por 10. O gerador de cartão de crédito do FakeForge BR implementa Luhn para Visa, Mastercard, Elo, Hipercard e Amex.
Correlação entre campos: CEP, DDD, estado e cidade precisam bater
Um endereço com CEP 01310-100 (Av. Paulista, São Paulo) e DDD 51 (Porto Alegre) quebra testes de validação de endereço e cálculo de frete. Dados correlacionados garantem que todos os campos de um registro sejam internamente consistentes.
Dados financeiros: chaves PIX, códigos ISPB/BACEN, agência e conta
O BACEN define o ISPB (Identificador de Sistema de Pagamentos Brasileiro) de cada instituição. Uma chave PIX do tipo evp tem formato UUID; do tipo cpf, 11 dígitos; do tipo telefone, +55 seguido de DDD e número; do tipo email, formato RFC 5322. Misturar formatos invalida a chave no ambiente de sandbox dos bancos. O gerador de PIX cobre os quatro formatos.
Requisitos do ambiente antes de começar
PostgreSQL 14+ e extensão pgcrypto (para UUIDs nativos)
-- Habilitar geração de UUID no banco
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Verificar versão
SELECT version();
-- PostgreSQL 14.x ou superior recomendadoNode.js 18+ ou Python 3.10+ para os scripts de seed
O script usa fetch nativo (disponível a partir do Node.js 18 sem flag experimental) e o driver postgres (também conhecido como postgres.js). Instale as dependências:
npm install postgres dotenvVariáveis de ambiente e conexão segura ao banco de staging
# .env (nunca commitar)
DATABASE_URL=postgresql://user:senha@staging-host:5432/staging_db?sslmode=require
FAKEFORGE_API_KEY=ff_live_xxxxxxxxxxxxxxxxAVISO: Nunca use sslmode=disable em staging. Conexões não criptografadas expõem credenciais e dados em trânsito, violando o Art. 46 da LGPD independente de o dado ser sintético ou real.Gerar os dados com a API FakeForge BR
Endpoint GET /api/generate — parâmetros type, quantity e format
A referência completa da API REST documenta todos os tipos disponíveis. A chamada mais simples:
GET https://fakeforge.com.br/api/generate?type=pessoa&quantity=100&format=json
Authorization: Bearer ff_live_xxxxxxxxxxxxxxxx| Parâmetro | Valores aceitos | Padrão |
|---|---|---|
type | cpf, cnpj, pessoa, empresa, pix, cartao, cep, conta | obrigatório |
quantity | 1 – 10.000 | 1 |
format | json, csv, sql | json |
Exportar direto em SQL com format=sql: CREATE TABLE já incluído
Com format=sql, a resposta inclui o CREATE TABLE IF NOT EXISTS e os INSERT INTO prontos para execução. Útil para ambientes onde você quer importar sem escrever DDL manualmente.
Autenticação com API key e limites de rate (100 chamadas/dia no plano gratuito)
O plano gratuito permite 100 chamadas por dia. O plano Dev (R$ 29/mês) sobe para 10.000 chamadas/dia. Para um seed de 1.000 pessoas + 500 empresas + 2.000 transações, o plano gratuito não é suficiente — planeje com o plano Dev ou faça cache local do resultado gerado.
Gerar em lote com script TypeScript usando fetch e loop por tipo
// src/seed/fetchData.ts
async function fetchWithRetry(
url: string,
apiKey: string,
retries = 3
): Promise<unknown[]> {
for (let attempt = 1; attempt <= retries; attempt++) {
const res = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (res.status === 429) {
const retryAfter = Number(res.headers.get("Retry-After") ?? 60);
console.log(`Rate limit atingido. Aguardando ${retryAfter}s...`);
await new Promise((r) => setTimeout(r, retryAfter * 1000));
continue;
}
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
const data = await res.json();
return data as unknown[];
}
throw new Error("Número máximo de tentativas atingido.");
}Modelar o schema PostgreSQL para dados brasileiros
Tipos corretos para CPF (CHAR(11)), CNPJ (CHAR(14)), CEP (CHAR(8))
CREATE TABLE pessoas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nome VARCHAR(120) NOT NULL,
cpf CHAR(11) NOT NULL UNIQUE,
email VARCHAR(254) NOT NULL,
telefone VARCHAR(15),
cep CHAR(8) NOT NULL,
logradouro VARCHAR(200),
cidade VARCHAR(100),
estado CHAR(2),
criado_em TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE empresas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
razao_social VARCHAR(200) NOT NULL,
cnpj CHAR(14) NOT NULL UNIQUE,
email VARCHAR(254),
cep CHAR(8),
cidade VARCHAR(100),
estado CHAR(2),
criado_em TIMESTAMPTZ DEFAULT NOW()
);
CREATE TYPE pix_tipo AS ENUM ('cpf', 'cnpj', 'telefone', 'email', 'evp');
CREATE TABLE transacoes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
pessoa_id UUID REFERENCES pessoas(id),
pix_chave VARCHAR(77),
pix_tipo pix_tipo,
banco_nome VARCHAR(100),
agencia VARCHAR(10),
conta VARCHAR(20),
cartao_numero CHAR(16),
cartao_bandeira VARCHAR(20),
criado_em TIMESTAMPTZ DEFAULT NOW()
);Constraints de CHECK para validar formato no banco
ALTER TABLE pessoas
ADD CONSTRAINT cpf_formato CHECK (cpf ~ '^\d{11}$'),
ADD CONSTRAINT cep_formato CHECK (cep ~ '^\d{8}$'),
ADD CONSTRAINT estado_formato CHECK (estado ~ '^[A-Z]{2}$');
ALTER TABLE empresas
ADD CONSTRAINT cnpj_formato CHECK (cnpj ~ '^\d{14}$');Script de seed completo em TypeScript
// src/seed/index.ts
import postgres from "postgres";
import "dotenv/config";
const sql = postgres(process.env.DATABASE_URL!);
const API_BASE = "https://fakeforge.com.br/api/generate";
const API_KEY = process.env.FAKEFORGE_API_KEY!;
async function fetchData(type: string, quantity: number): Promise<unknown[]> {
const url = `${API_BASE}?type=${type}&quantity=${quantity}&format=json`;
const res = await fetch(url, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
if (!res.ok) throw new Error(`Erro ao buscar ${type}: HTTP ${res.status}`);
return res.json() as Promise<unknown[]>;
}
async function seedPessoas() {
const pessoas = await fetchData("pessoa", 1000) as Array<{
nome: string; cpf: string; email: string; telefone: string;
cep: string; logradouro: string; cidade: string; estado: string;
}>;
await sql`
INSERT INTO pessoas (nome, cpf, email, telefone, cep, logradouro, cidade, estado)
SELECT nome, cpf, email, telefone, cep, logradouro, cidade, estado
FROM ${sql(pessoas)}
ON CONFLICT (cpf) DO NOTHING
`;
console.log(`Pessoas inseridas: até ${pessoas.length}`);
}
async function seedEmpresas() {
const empresas = await fetchData("empresa", 500) as Array<{
razao_social: string; cnpj: string; email: string;
cep: string; cidade: string; estado: string;
}>;
await sql`
INSERT INTO empresas (razao_social, cnpj, email, cep, cidade, estado)
SELECT razao_social, cnpj, email, cep, cidade, estado
FROM ${sql(empresas)}
ON CONFLICT (cnpj) DO NOTHING
`;
console.log(`Empresas inseridas: até ${empresas.length}`);
}
async function seedTransacoes() {
const [pix, contas] = await Promise.all([
fetchData("pix", 2000),
fetchData("conta", 2000),
]);
// Buscar IDs de pessoas para associar às transações
const ids = await sql<{ id: string }[]>`SELECT id FROM pessoas LIMIT 2000`;
const rows = ids.map((p, i) => ({
pessoa_id: p.id,
pix_chave: (pix[i] as { chave: string }).chave,
pix_tipo: (pix[i] as { tipo: string }).tipo,
banco_nome: (contas[i] as { banco: string }).banco,
agencia: (contas[i] as { agencia: string }).agencia,
conta: (contas[i] as { conta: string }).conta,
}));
await sql`
INSERT INTO transacoes (pessoa_id, pix_chave, pix_tipo, banco_nome, agencia, conta)
SELECT pessoa_id, pix_chave, pix_tipo::pix_tipo, banco_nome, agencia, conta
FROM ${sql(rows)}
ON CONFLICT DO NOTHING
`;
console.log(`Transações inseridas: até ${rows.length}`);
}
async function main() {
await seedPessoas();
await seedEmpresas();
await seedTransacoes();
await sql.end();
}
main().catch((err) => { console.error(err); process.exit(1); });O ON CONFLICT DO NOTHING garante idempotência: rodar o script duas vezes não duplica registros.
Usar o export SQL direto do FakeForge BR
Baixar o arquivo .sql pela UI ou via API com format=sql
curl -s \
"https://fakeforge.com.br/api/generate?type=pessoa&quantity=500&format=sql" \
-H "Authorization: Bearer $FAKEFORGE_API_KEY" \
-o seed_pessoas.sqlExecutar com psql -f seed.sql ou via migration (Flyway/Liquibase)
psql "$DATABASE_URL" -f seed_pessoas.sqlPara Flyway, salve o arquivo como V2__seed_staging.sql no diretório de migrations. O prefixo V indica migration versionada; use R (repeatable) se quiser reexecutar a cada sprint.
Adaptar o DDL gerado ao schema existente do projeto
O CREATE TABLE IF NOT EXISTS gerado pelo FakeForge BR usa nomes de coluna em português. Se o schema do seu projeto usa inglês, ajuste com sed antes de importar ou edite o arquivo manualmente. O DDL gerado é um ponto de partida, não um requisito.
Automatizar o seed em CI/CD
GitHub Actions: step de seed antes dos testes de integração
# .github/workflows/integration.yml
name: Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: ci
POSTGRES_PASSWORD: ci
POSTGRES_DB: staging_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- name: Aplicar migrations
env:
DATABASE_URL: postgresql://ci:ci@localhost:5432/staging_test
run: npm run migrate
- name: Seed com dados sintéticos
env:
DATABASE_URL: postgresql://ci:ci@localhost:5432/staging_test
FAKEFORGE_API_KEY: ${{ secrets.SEED_API_KEY }}
run: npx ts-node src/seed/index.ts
- name: Rodar testes de integração
env:
DATABASE_URL: postgresql://ci:ci@localhost:5432/staging_test
run: npm run test:integrationDICA: ArmazeneSEED_API_KEYnos GitHub Secrets (Settings → Secrets → Actions). Nunca coloque a key no código ou em arquivos.envcommitados.
Docker Compose com init script para ambiente local
# docker-compose.yml
services:
db:
image: postgres:16
environment:
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
POSTGRES_DB: staging
volumes:
- ./sql/init:/docker-entrypoint-initdb.d
ports:
- "5432:5432"Coloque os arquivos .sql exportados pelo FakeForge BR em ./sql/init/. O PostgreSQL executa todos os arquivos *.sql nesse diretório na ordem alfabética ao criar o container pela primeira vez.
Validar os dados inseridos no banco
Query SQL para checar CPFs com dígito verificador inválido
-- Retorna CPFs que não passam na verificação de formato básico
-- (validação completa do mod-11 requer função PL/pgSQL)
SELECT cpf, nome
FROM pessoas
WHERE cpf !~ '^\d{11}$'
OR cpf = repeat(substring(cpf, 1, 1), 11); -- rejeita sequências como '11111111111'Para validação completa do mod-11 dentro do banco, implemente uma função is_cpf_valido(text) em PL/pgSQL ou use o endpoint /validar-cpf em um script de smoke test pós-seed.
Testar correlação CEP × estado com JOIN em tabela de referência
-- Prefixos de CEP por estado (amostra)
CREATE TEMP TABLE cep_estados (prefixo CHAR(2), estado CHAR(2));
INSERT INTO cep_estados VALUES
('01', 'SP'), ('20', 'RJ'), ('30', 'MG'),
('40', 'BA'), ('60', 'CE'), ('70', 'DF'),
('80', 'PR'), ('90', 'RS');
SELECT p.cpf, p.cep, p.estado, ce.estado AS estado_esperado
FROM pessoas p
LEFT JOIN cep_estados ce ON LEFT(p.cep, 2) = ce.prefixo
WHERE ce.estado IS DISTINCT FROM p.estado
LIMIT 20;Endpoint /validar-cpf e /validar-cnpj para smoke test pós-seed
Após o seed, chame os endpoints de validação de CPF e validação de CNPJ em uma amostra aleatória para confirmar que os dados gerados são válidos do ponto de vista do mod-11.
Boas práticas de segurança e conformidade LGPD
Isolar banco de staging na VPC — sem rota pública
O banco de staging nunca deve ter endpoint público. Configure security groups (AWS) ou firewall rules (GCP/Azure) para aceitar conexões apenas de dentro da VPC privada. Mesmo dados sintéticos expostos publicamente criam superfície de ataque desnecessária.
Nunca promover dados de staging para produção (LGPD Art. 46)
O Art. 46 da LGPD exige que o controlador implemente medidas técnicas para proteger dados pessoais. Promover um dump de staging para produção — mesmo com dados sintéticos — cria confusão operacional e pode misturar dados se o processo não for rigoroso. Separe os ambientes por banco, por credencial e por pipeline de deploy.
Rotation automático do seed a cada sprint para evitar fixação de padrões
Dados fixos por meses criam memorização involuntária: desenvolvedores decoram CPFs e CNPJs específicos, testes passam por acidente porque assumem valores hardcoded. Automatize o re-seed a cada início de sprint com um job agendado no CI/CD.
Resumo
- Schema primeiro: defina
CHAR(11)para CPF,CHAR(14)para CNPJ,CHAR(8)para CEP e use constraints deCHECKpara garantir formato no banco antes de inserir qualquer dado. - API FakeForge BR: use o endpoint
/api/generatecomformat=jsonpara seed programático ouformat=sqlpara importação direta viapsql -f. - Idempotência: sempre use
ON CONFLICT DO NOTHINGnosINSERT INTOdo script de seed para poder reexecutar sem duplicar registros. - CI/CD: adicione o step de seed como pré-condição dos testes de integração no GitHub Actions, com a API key em
secrets.SEED_API_KEYe nunca em texto plano. - Validação pós-seed: execute as queries de verificação de formato e correlação CEP × estado, e chame os endpoints /validar-cpf e /validar-cnpj em amostra antes de liberar o ambiente para os testes.
- Compliance: isole o banco de staging na VPC, nunca promova dados para produção e rotacione o seed a cada sprint — requisitos derivados da LGPD Art. 7º e Art. 46, não apenas boas práticas.
Perguntas frequentes
Como reutilizar dados gerados para economizar rate limit do plano gratuito?+
Gere uma única vez com `format=sql`, commit o arquivo no repositório (ou em artifact do CI), e reutilize em todas as pipelines. Ou cache o resultado JSON em S3/GitHub Actions artifacts. Evita chamar a API múltiplas vezes por PR/branch.
Qual a ordem correta de execução do seed: pessoas → empresas → transações?+
Pessoas primeiro (sem foreign keys), depois empresas (FK pessoa), depois transações (FK pessoa+empresa). Use `await Promise.all()` apenas entre jobs independentes. Antes de inserir transações, execute `SELECT id FROM pessoas` no banco para coletar IDs reais e garantir integridade de FK.
Como garanto que pessoa_id nas transações realmente existe no banco?+
Não gere UUIDs client-side aleatoriamente. Execute `SELECT id FROM pessoas LIMIT X` no script TypeScript antes de montar os rows de transações. Ou use gerador que retorna relacionamentos já pré-conectados no JSON (pessoa_id + transação). Evita violação de FK.
Se dados são sintéticos em staging, por que ainda precisam de isolamento LGPD?+
Sim. LGPD Art. 46 exige proteção para *qualquer* dado estruturado como pessoal, sintético ou não. Implemente VPC privada, nenhum endpoint público, zero credenciais em código, RBAC para acesso. Compliance é sobre *processo* de tratamento, não apenas se valor é factual.
Qual é a forma recomendada para validar CPF mod-11 dentro do PostgreSQL?+
Cria função PL/pgSQL com o algoritmo mod-11 completo ou usa trigger BEFORE INSERT para rejeitar inválidos. Mais prático: valida em application-side (TypeScript) antes do INSERT, usando a biblioteca interna do FakeForge; é mais testável, reutilizável e rápido.