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

**Publicado:** 17/08/2024
**Autor:** Redação CPFHub.io
**URL:** https://cpfhub.io/blog/armazenar-dados-api-cpf-postgresql-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.

```ruby
# 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.

```ruby
# 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.

```ruby
# 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.

```ruby
# 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)](https://www.planalto.gov.br/ccivil_03/_ato2015-2018/2018/lei/l13709.htm) também exige que dados pessoais não sejam retidos por prazo superior ao necessário para a finalidade declarada.

```ruby
# 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.

### Leia também

- [Como validar CPF no frontend com React e API REST](https://cpfhub.io/blog/como-validar-cpf-no-frontend-com-react-e-api-rest)
- [SLA de API de CPF: níveis de disponibilidade e o que exigir do seu provedor](https://cpfhub.io/blog/sla-api-cpf-niveis-disponibilidade)
- [API de CPF grátis para desenvolvedores: como começar em 5 minutos](https://cpfhub.io/blog/api-cpf-gratis-desenvolvedores-comecar-5-minutos)
- [10 erros mais comuns ao integrar uma API de CPF e como evitá-los](https://cpfhub.io/blog/10-erros-mais-comuns-ao-integrar-uma-api-de-cpf)

---

## 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](https://www.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.

