← Voltar ao blog

Popular PostgreSQL com dados brasileiros para staging

·12 min de leitura

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 recomendado

Node.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 dotenv

Variá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_xxxxxxxxxxxxxxxx
AVISO: 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âmetroValores aceitosPadrão
typecpf, cnpj, pessoa, empresa, pix, cartao, cep, contaobrigatório
quantity1 – 10.0001
formatjson, csv, sqljson

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.sql

Executar com psql -f seed.sql ou via migration (Flyway/Liquibase)

psql "$DATABASE_URL" -f seed_pessoas.sql

Para 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:integration
DICA: Armazene SEED_API_KEY nos GitHub Secrets (Settings → Secrets → Actions). Nunca coloque a key no código ou em arquivos .env commitados.

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 de CHECK para garantir formato no banco antes de inserir qualquer dado.
  • API FakeForge BR: use o endpoint /api/generate com format=json para seed programático ou format=sql para importação direta via psql -f.
  • Idempotência: sempre use ON CONFLICT DO NOTHING nos INSERT INTO do 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_KEY e 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.