Como Armazenar Dados da API de CPF no PostgreSQL em Aplicações Ruby

Aprenda a armazenar dados retornados pela API de CPF no PostgreSQL em aplicações Ruby, com modelos, índices, criptografia e boas práticas.

Redação CPFHub.io
Redação CPFHub.io
··7 min de leitura
Como Armazenar Dados da API de CPF no PostgreSQL em Aplicações Ruby

Após consultar a API de CPF, é essencial armazenar os resultados de forma estruturada para evitar consultas repetidas, manter histórico de validações e possibilitar análises futuras. O PostgreSQL, combinado com Ruby e ActiveRecord, oferece recursos avançados como criptografia em nível de coluna, índices parciais e JSONB para armazenar esses dados de forma segura e eficiente.

Introdução

Armazenar os resultados das consultas de CPF localmente permite que sua aplicação reduza chamadas desnecessárias à API, melhore a performance e mantenha um histórico auditável de validações. A combinação de PostgreSQL com Ruby e ActiveRecord oferece o equilíbrio ideal entre segurança, flexibilidade e desempenho para essa finalidade.

Modelagem do banco de dados

A estrutura deve acomodar tanto os dados retornados pela API quanto metadados de controle.

# db/migrate/001_create_consultas_cpf.rb
class CreateConsultasCpf < ActiveRecord::Migration[7.1]
    def change
    create_table :consultas_cpf do |t|
    t.string :cpf_hash, null: false
    t.text :cpf_cifrado
    t.string :nome
    t.string :nome_upper
    t.string :genero
    t.date :data_nascimento
    t.integer :dia_nascimento
    t.integer :mes_nascimento
    t.integer :ano_nascimento
    t.string :status, null: false, default: "pendente"
    t.text :motivo_falha
    t.string :origem, null: false
    t.jsonb :resposta_completa, default: {}
    t.datetime :consultado_em
    t.datetime :expira_em

    t.timestamps
    end

    add_index :consultas_cpf, :cpf_hash, unique: true
    add_index :consultas_cpf, :status
    add_index :consultas_cpf, :expira_em
    add_index :consultas_cpf, :resposta_completa, using: :gin
    add_index :consultas_cpf, :consultado_em,
    where: "status = 'sucesso'",
    name: "idx_consultas_cpf_sucesso"
    end
end
ColunaTipoDescrição
cpf_hashstringHash SHA-256 do CPF (para busca)
cpf_cifradotextCPF criptografado com AES-256
nomestringNome retornado pela API
generostringGênero retornado pela API
data_nascimentodateData de nascimento completa
statusstringpendente, sucesso, falha
origemstringcheckout, cadastro, lote, etc.
resposta_completajsonbResposta integral da API
expira_emdatetimeQuando o cache expira

Model com criptografia e validações

O model ActiveRecord implementa criptografia do CPF e métodos de consulta.

# app/models/consulta_cpf.rb
class ConsultaCpf < ApplicationRecord
    # Criptografia nativa do Rails 7+
    encrypts :cpf_cifrado, deterministic: false

    # Validações
    validates :cpf_hash, presence: true, uniqueness: true
    validates :status, inclusion: { in: %w[pendente sucesso falha] }
    validates :origem, presence: true

    # Scopes
    scope :validas, -> { where(status: "sucesso").where("expira_em > ?", Time.current) }
    scope :expiradas, -> { where("expira_em <= ?", Time.current) }
    scope :por_origem, ->(origem) { where(origem: origem) }

    # Callbacks
    before_validation :gerar_hash_cpf, on: :create
    before_create :definir_expiracao

    # Método para buscar por CPF (usando hash)
    def self.buscar_por_cpf(cpf)
    hash = gerar_hash(cpf)
    validas.find_by(cpf_hash: hash)
    end

    def self.gerar_hash(cpf)
    cpf_limpo = cpf.to_s.gsub(/\D/, "")
    Digest::SHA256.hexdigest("#{cpf_limpo}:#{ENV['CPF_HASH_SALT']}")
    end

    def expirada?
    expira_em.present? && expira_em <= Time.current
    end

    def cpf_parcial
    cpf_decifrado = cpf_cifrado
    return nil unless cpf_decifrado

    "#{cpf_decifrado[0..2]}.***.***-#{cpf_decifrado[-2..]}"
    end

    private

    def gerar_hash_cpf
    self.cpf_hash = self.class.gerar_hash(cpf_cifrado) if cpf_cifrado.present?
    end

    def definir_expiracao
    self.expira_em ||= 24.hours.from_now
    end
end

Service para consulta e armazenamento

O service orquestra a consulta à API e o armazenamento no PostgreSQL.

# app/services/cpf_lookup_service.rb
class CpfLookupService
    TTL_SUCESSO = 24.hours
    TTL_FALHA = 1.hour

    def initialize(api_key: ENV["CPFHUB_API_KEY"])
    @api_key = api_key
    @connection = build_connection
    end

    def consultar(cpf, origem: "sistema")
    cpf_limpo = cpf.to_s.gsub(/\D/, "")

    # Verificar cache no banco
    cached = ConsultaCpf.buscar_por_cpf(cpf_limpo)
    return resultado_cache(cached) if cached

    # Consultar API
    resposta = chamar_api(cpf_limpo)
    resultado = JSON.parse(resposta.body)

    # Armazenar no banco
    registro = persistir_resultado(cpf_limpo, resultado, origem)

    {
    fonte: "api",
    dados: registro,
    sucesso: resultado["success"]
    }
    rescue Faraday::Error => e
    registrar_falha(cpf_limpo, origem, e.message)
    { fonte: "erro", sucesso: false, erro: e.message }
    end

    private

    def build_connection
    Faraday.new(url: "https://api.cpfhub.io") do |conn|
    conn.headers["x-api-key"] = @api_key
    conn.options.timeout = 10
    conn.adapter Faraday.default_adapter
    end
    end

    def chamar_api(cpf)
    @connection.get("/cpf/#{cpf}")
    end

    def persistir_resultado(cpf, resultado, origem)
    if resultado["success"]
    dados = resultado["data"]
    ConsultaCpf.create!(
    cpf_cifrado: cpf,
    nome: dados["name"],
    nome_upper: dados["nameUpper"],
    genero: dados["gender"],
    data_nascimento: dados["birthDate"],
    dia_nascimento: dados["day"],
    mes_nascimento: dados["month"],
    ano_nascimento: dados["year"],
    status: "sucesso",
    origem: origem,
    resposta_completa: dados,
    consultado_em: Time.current,
    expira_em: TTL_SUCESSO.from_now
    )
    else
    registrar_falha(cpf, origem, "CPF nao encontrado")
    end
    end

    def registrar_falha(cpf, origem, motivo)
    ConsultaCpf.create!(
    cpf_cifrado: cpf,
    status: "falha",
    motivo_falha: motivo,
    origem: origem,
    consultado_em: Time.current,
    expira_em: TTL_FALHA.from_now
    )
    end

    def resultado_cache(registro)
    {
    fonte: "banco",
    dados: registro,
    sucesso: true
    }
    end
end

Consultas avançadas com PostgreSQL

O PostgreSQL oferece recursos que facilitam consultas analíticas sobre os dados armazenados.

# Consultas analíticas úteis

# Total de consultas por status
ConsultaCpf.group(:status).count
# => {"sucesso" => 15420, "falha" => 832, "pendente" => 45}

# Distribuição por gênero
ConsultaCpf.validas.group(:genero).count
# => {"M" => 8521, "F" => 6899}

# Distribuição por faixa etária usando JSONB
ConsultaCpf.validas
    .select("EXTRACT(YEAR FROM AGE(data_nascimento)) AS idade")
    .group("CASE
    WHEN EXTRACT(YEAR FROM AGE(data_nascimento)) < 25 THEN '18-24'
    WHEN EXTRACT(YEAR FROM AGE(data_nascimento)) < 35 THEN '25-34'
    WHEN EXTRACT(YEAR FROM AGE(data_nascimento)) < 45 THEN '35-44'
    WHEN EXTRACT(YEAR FROM AGE(data_nascimento)) < 55 THEN '45-54'
    ELSE '55+'
    END")
    .count

# Busca em JSONB
ConsultaCpf.where("resposta_completa @> ?", { gender: "M" }.to_json)
Tipo de ÍndiceColunaUso
B-tree (unique)cpf_hashBusca por CPF específico
B-treestatusFiltragem por status
B-treeexpira_emLimpeza de registros expirados
GINresposta_completaBuscas em JSONB
Parcialconsultado_em (status=sucesso)Consultas apenas em sucessos

Limpeza e manutenção

Registros expirados devem ser limpos periodicamente para manter o banco performático. A Lei Geral de Proteção de Dados (LGPD) também exige que dados pessoais não sejam retidos por prazo superior ao necessário para a finalidade declarada.

# app/jobs/limpar_consultas_expiradas_job.rb
class LimparConsultasExpiradasJob < ApplicationJob
    queue_as :manutencao

    def perform
    total_removidos = ConsultaCpf.expiradas.delete_all

    Rails.logger.info(
    "[Manutencao] #{total_removidos} consultas expiradas removidas"
    )

    # Analisar tabela após remoção em massa
    ActiveRecord::Base.connection.execute(
    "ANALYZE consultas_cpf"
    )
    end
end

# config/schedule.rb (usando whenever)
every 1.day, at: "3:00 am" do
    runner "LimparConsultasExpiradasJob.perform_later"
end

Perguntas frequentes

Por que usar SHA-256 para armazenar o CPF em vez do valor bruto?

O hash SHA-256 permite buscar registros por CPF sem armazenar o número em texto claro no banco. Combinado com um salt de ambiente (CPF_HASH_SALT), o hash não pode ser revertido por ataques de dicionário — o CPF bruto é guardado apenas na coluna criptografada, acessível somente pela aplicação.

Qual é a diferença entre cpf_hash e cpf_cifrado no modelo?

cpf_hash é um SHA-256 determinístico usado para indexação e buscas rápidas. cpf_cifrado usa criptografia AES-256 não-determinística do Rails 7 e serve para recuperar o CPF real quando necessário (por exemplo, para reenviar à API). Os dois campos trabalham juntos: busque pelo hash, descriptografe somente quando precisar do valor.

Com que frequência os registros de CPF devem ser revalidados?

O TTL padrão recomendado é de 24 horas para consultas bem-sucedidas e 1 hora para falhas. Cadastros com mais de 30 dias devem ser revalidados ativamente, pois dados cadastrais (como situação na Receita Federal) podem mudar. O job de revalidação diária garante que o cache reflita o estado atual.

Como escalar o armazenamento para volumes acima de 100 mil consultas por mês?

Use o índice parcial idx_consultas_cpf_sucesso para filtrar apenas registros bem-sucedidos em queries de análise. Para volumes muito altos, considere particionamento por consultado_em no PostgreSQL e ajuste o ANALYZE para rodar automaticamente via autovacuum. O índice GIN sobre resposta_completa acelera buscas em campos JSONB sem necessidade de colunas adicionais.


Conclusão

Armazenar dados da API de CPF no PostgreSQL com Ruby oferece o melhor dos dois mundos: a conveniência do cache para evitar consultas repetidas e a segurança de criptografia em nível de coluna para proteger dados sensíveis. O uso de JSONB para a resposta completa, índices parciais para consultas frequentes e jobs de limpeza para manutenção garante uma solução robusta e performática para aplicações de qualquer porte.

Cadastre-se em cpfhub.io — 50 consultas mensais gratuitas, sem cartão de crédito — e comece a armazenar resultados de validação de CPF de forma segura e eficiente na sua aplicação Ruby.

CPFHub.io

Pronto para integrar a API?

50 consultas gratuitas para testar agora. Sem cartão de crédito. Acesso imediato à documentação.

Redação CPFHub.io

Sobre a redação

Redação CPFHub.io

Time editorial especializado em APIs de CPF, identidade digital e compliance no mercado brasileiro. Produzimos guias técnicos, análises regulatórias e tutoriais sobre LGPD e KYC para desenvolvedores e líderes de produto.

WhatsAppFale conosco via WhatsApp