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
| Coluna | Tipo | Descrição |
|---|---|---|
| cpf_hash | string | Hash SHA-256 do CPF (para busca) |
| cpf_cifrado | text | CPF criptografado com AES-256 |
| nome | string | Nome retornado pela API |
| genero | string | Gênero retornado pela API |
| data_nascimento | date | Data de nascimento completa |
| status | string | pendente, sucesso, falha |
| origem | string | checkout, cadastro, lote, etc. |
| resposta_completa | jsonb | Resposta integral da API |
| expira_em | datetime | Quando 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 Índice | Coluna | Uso |
|---|---|---|
| B-tree (unique) | cpf_hash | Busca por CPF específico |
| B-tree | status | Filtragem por status |
| B-tree | expira_em | Limpeza de registros expirados |
| GIN | resposta_completa | Buscas em JSONB |
| Parcial | consultado_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.
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.



