Progressive Web Apps com estratégia offline-first permitem validar CPFs mesmo sem conexão de internet, usando Service Workers para interceptar requisições e IndexedDB para armazenar resultados em cache. A validação local dos dígitos verificadores funciona 100% offline, enquanto a consulta completa na API CPFHub.io é sincronizada automaticamente quando a conexão é restabelecida.
Introdução
Em um país com dimensões continentais como o Brasil, nem sempre é possível contar com uma conexão de internet estável. Vendedores em campo, agentes de saúde em áreas rurais e profissionais em eventos com Wi-Fi congestionado precisam validar CPFs mesmo quando estão offline ou com conexão intermitente.
PWAs com estratégia offline-first resolvem esse problema. A aplicação funciona normalmente sem internet — fazendo validação local de dígitos verificadores — e sincroniza as consultas com a API da CPFHub.io quando a conexão volta. A especificação de Service Workers do W3C define o padrão técnico que viabiliza esse comportamento.
Arquitetura offline-first para validação de CPF
A estratégia offline-first inverte a lógica tradicional: em vez de tentar conectar à API e tratar a falha como exceção, o sistema assume que pode estar offline e trata a conexão como um bônus.
Camadas de validação
- Camada local (sempre disponível) — validação de formato e dígitos verificadores via algoritmo mod-11.
- Cache local (disponível após primeira consulta) — CPFs previamente consultados ficam armazenados em IndexedDB.
- API remota (disponível com internet) — consulta completa com nome, data de nascimento e gênero.
Configurando o Service Worker
O Service Worker é o coração da PWA. Ele intercepta requisições de rede e decide se deve usar o cache ou a rede.
// sw.js
const CACHE_NAME = 'cpf-validator-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/manifest.json'
];
// Instalar: cachear assets estaticos
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS))
);
self.skipWaiting();
});
// Ativar: limpar caches antigos
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((names) =>
Promise.all(
names
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
)
)
);
self.clients.claim();
});
// Fetch: estrategia network-first para API, cache-first para assets
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.hostname === 'api.cpfhub.io') {
// API: network-first com fallback para cache
event.respondWith(networkFirstAPI(event.request));
} else {
// Assets: cache-first
event.respondWith(
caches.match(event.request).then((cached) => cached || fetch(event.request))
);
}
});
async function networkFirstAPI(request) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(request, { signal: controller.signal });
clearTimeout(timeoutId);
// Cachear resposta bem-sucedida
if (response.ok) {
const cache = await caches.open('cpf-api-cache');
cache.put(request, response.clone());
}
return response;
} catch (err) {
clearTimeout(timeoutId);
// Fallback: tentar cache
const cached = await caches.match(request);
if (cached) return cached;
// Sem cache: retornar resposta offline
return new Response(
JSON.stringify({
success: false,
offline: true,
message: 'Consulta offline. Validacao local realizada.'
}),
{ headers: { 'Content-Type': 'application/json' } }
);
}
}
Registrando o Service Worker
// app.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('SW registrado:', registration.scope);
} catch (err) {
console.error('Erro ao registrar SW:', err);
}
});
}
Cache de consultas com IndexedDB
Para armazenar resultados de consultas de CPF de forma persistente, usamos IndexedDB — que suporta dados estruturados e não tem os limites de tamanho do localStorage.
// db.js
class CPFDatabase {
constructor() {
this.dbName = 'cpf-validator';
this.storeName = 'consultas';
this.db = null;
}
async open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, { keyPath: 'cpf' });
store.createIndex('timestamp', 'timestamp');
}
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve(this.db);
};
request.onerror = () => reject(request.error);
});
}
async salvar(cpf, dados) {
const tx = this.db.transaction(this.storeName, 'readwrite');
const store = tx.objectStore(this.storeName);
store.put({
cpf,
dados,
timestamp: Date.now(),
sincronizado: true
});
return new Promise((resolve) => { tx.oncomplete = resolve; });
}
async buscar(cpf) {
const tx = this.db.transaction(this.storeName, 'readonly');
const store = tx.objectStore(this.storeName);
const request = store.get(cpf);
return new Promise((resolve) => {
request.onsuccess = () => {
const result = request.result;
if (!result) { resolve(null); return; }
// Cache valido por 24 horas
const umDia = 24 * 60 * 60 * 1000;
if (Date.now() - result.timestamp > umDia) {
resolve(null);
return;
}
resolve(result.dados);
};
request.onerror = () => resolve(null);
});
}
async salvarPendente(cpf) {
const tx = this.db.transaction(this.storeName, 'readwrite');
const store = tx.objectStore(this.storeName);
store.put({
cpf,
dados: null,
timestamp: Date.now(),
sincronizado: false
});
return new Promise((resolve) => { tx.oncomplete = resolve; });
}
async getPendentes() {
const tx = this.db.transaction(this.storeName, 'readonly');
const store = tx.objectStore(this.storeName);
const request = store.getAll();
return new Promise((resolve) => {
request.onsuccess = () => {
resolve(request.result.filter((item) => !item.sincronizado));
};
});
}
}
const cpfDB = new CPFDatabase();
Fluxo de consulta offline-first
A função principal de consulta implementa a lógica de três camadas.
async function consultarCPFOfflineFirst(digits) {
await cpfDB.open();
// Camada 1: Validacao local
if (!validarDigitosCPF(digits)) {
return { success: false, error: 'CPF invalido (validacao local).' };
}
// Camada 2: Cache local
const cached = await cpfDB.buscar(digits);
if (cached) {
return { success: true, data: cached, source: 'cache' };
}
// Camada 3: API remota
if (navigator.onLine) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const res = await fetch(`https://api.cpfhub.io/cpf/${digits}`, {
headers: {
'x-api-key': 'SUA_CHAVE_DE_API',
'Accept': 'application/json'
},
signal: controller.signal
});
clearTimeout(timeoutId);
const json = await res.json();
if (json.success) {
await cpfDB.salvar(digits, json.data);
return { success: true, data: json.data, source: 'api' };
}
return { success: false, error: 'CPF nao encontrado.' };
} catch (err) {
clearTimeout(timeoutId);
// Falha na rede -- salvar como pendente
await cpfDB.salvarPendente(digits);
return {
success: true,
data: null,
source: 'offline',
message: 'CPF valido localmente. Consulta completa sera feita quando houver conexao.'
};
}
}
// Sem internet -- salvar como pendente
await cpfDB.salvarPendente(digits);
return {
success: true,
data: null,
source: 'offline',
message: 'Voce esta offline. O CPF e valido localmente e sera consultado quando a conexao voltar.'
};
}
function validarDigitosCPF(cpf) {
if (cpf.length !== 11 || /^(\d)\1{10}$/.test(cpf)) return false;
let soma = 0;
for (let i = 0; i < 9; i++) soma += parseInt(cpf[i]) * (10 - i);
let resto = (soma * 10) % 11;
if (resto === 10) resto = 0;
if (resto !== parseInt(cpf[9])) return false;
soma = 0;
for (let i = 0; i < 10; i++) soma += parseInt(cpf[i]) * (11 - i);
resto = (soma * 10) % 11;
if (resto === 10) resto = 0;
return resto === parseInt(cpf[10]);
}
Sincronização em background
Quando a conexão é restabelecida, a PWA precisa sincronizar as consultas pendentes. A Background Sync API é ideal para isso.
// No app.js: registrar sync quando voltar online
window.addEventListener('online', async () => {
if ('serviceWorker' in navigator && 'SyncManager' in window) {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-cpf-pendentes');
} else {
// Fallback: sincronizar diretamente
sincronizarPendentes();
}
});
async function sincronizarPendentes() {
await cpfDB.open();
const pendentes = await cpfDB.getPendentes();
for (const item of pendentes) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const res = await fetch(`https://api.cpfhub.io/cpf/${item.cpf}`, {
headers: {
'x-api-key': 'SUA_CHAVE_DE_API',
'Accept': 'application/json'
},
signal: controller.signal
});
clearTimeout(timeoutId);
const json = await res.json();
if (json.success) {
await cpfDB.salvar(item.cpf, json.data);
notificarUsuario(item.cpf, json.data);
}
} catch (err) {
clearTimeout(timeoutId);
// Tentar novamente na proxima sincronizacao
}
}
}
function notificarUsuario(cpf, dados) {
if ('Notification' in window && Notification.permission === 'granted') {
new Notification('CPF sincronizado', {
body: `O CPF ${cpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4')} foi verificado: ${dados.name}`,
icon: '/icons/icon-192.png'
});
}
}
Indicador de status de conexão
A interface deve comunicar claramente se o usuário está online ou offline.
<div class="connection-status" id="connectionStatus">
<span class="status-dot"></span>
<span class="status-text"></span>
</div>
<style>
.connection-status {
position: fixed;
top: 0;
left: 0;
right: 0;
padding: 6px 12px;
font-size: 0.8rem;
text-align: center;
transition: transform 0.3s ease, background 0.3s ease;
z-index: 1000;
}
.connection-status.online {
background: #d4edda;
color: #155724;
transform: translateY(-100%);
}
.connection-status.offline {
background: #fff3cd;
color: #856404;
transform: translateY(0);
}
.connection-status.syncing {
background: #cce5ff;
color: #004085;
transform: translateY(0);
}
.status-dot {
display: inline-block;
width: 8px; height: 8px;
border-radius: 50%;
margin-right: 6px;
vertical-align: middle;
}
.online .status-dot { background: #28a745; }
.offline .status-dot { background: #ffc107; }
.syncing .status-dot { background: #007bff; animation: pulse 1s infinite; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
</style>
<script>
function updateConnectionStatus() {
const el = document.getElementById('connectionStatus');
const dot = el.querySelector('.status-dot');
const text = el.querySelector('.status-text');
if (navigator.onLine) {
el.className = 'connection-status online';
text.textContent = 'Online';
// Esconder apos 2 segundos
setTimeout(() => { el.style.transform = 'translateY(-100%)'; }, 2000);
} else {
el.className = 'connection-status offline';
text.textContent = 'Offline -- validacao local ativa';
}
}
window.addEventListener('online', updateConnectionStatus);
window.addEventListener('offline', updateConnectionStatus);
updateConnectionStatus();
</script>
Web App Manifest
Para que a PWA seja instalável, configure o manifest.
{
"name": "Validador de CPF",
"short_name": "CPF Validator",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3498db",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
Perguntas frequentes
Como o Service Worker decide quando usar cache ou rede para consultas de CPF?
A estratégia network-first tenta a rede primeiro com timeout de 10 segundos. Se a requisição falha ou o dispositivo está offline, o Service Worker busca a resposta no cache do CPF consultado anteriormente. Apenas quando nem rede nem cache estão disponíveis, o sistema retorna uma resposta offline indicando que a validação local foi realizada.
É possível usar IndexedDB offline sem limites de armazenamento?
O IndexedDB não tem limite fixo no padrão — o navegador pode alocar até uma fração do armazenamento disponível no dispositivo. Na prática, para uma PWA de validação de CPF, cada registro ocupa menos de 1 KB, então milhares de consultas cabem com folga. Mantenha limpeza automática de registros com mais de 24 horas para evitar acúmulo desnecessário.
A Background Sync API funciona em todos os navegadores?
A Background Sync API tem suporte nativo no Chrome e Edge. No Firefox e Safari, o código usa o fallback direto — o evento online dispara sincronizarPendentes() assim que a conexão volta. A implementação no artigo já contempla essa compatibilidade com o bloco else do SyncManager.
O que acontece se o usuário consultar o mesmo CPF offline várias vezes?
A função consultarCPFOfflineFirst verifica primeiro o cache local no IndexedDB. Se o CPF já foi consultado nas últimas 24 horas, retorna o resultado em cache sem criar novas entradas pendentes. Apenas CPFs sem registro no cache são marcados como pendentes para sincronização posterior.
Conclusão
A combinação de PWA com estratégia offline-first resolve um problema real para profissionais que precisam validar CPFs em campo, sem garantia de conexão estável. A validação local por dígitos verificadores funciona 100% offline, o IndexedDB armazena consultas anteriores e a Background Sync garante que tudo seja reconciliado quando a internet voltar.
A API da CPFHub.io responde em ~900ms e se encaixa perfeitamente nessa arquitetura: quando há conexão, a consulta completa retorna nome, data de nascimento e gênero do titular. Quando não há, o fluxo offline-first assume o controle sem interromper a experiência do usuário.
Teste gratuitamente em cpfhub.io — 50 consultas por mês sem cartão de crédito, sem compromisso.
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.



