# Como usar Progressive Web App (PWA) para validação de CPF offline-first

> Aprenda a criar uma PWA para validação de CPF com estratégia offline-first, usando Service Workers e sincronização em background.

**Publicado:** 13/01/2026
**Autor:** Redação CPFHub.io
**URL:** https://cpfhub.io/blog/como-usar-progressive-web-app-pwa-para-validacao-de-cpf-offline-first

---


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**](https://www.cpfhub.io/) quando a conexão volta. A [especificação de Service Workers do W3C](https://www.w3.org/TR/service-workers/) 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

1. **Camada local (sempre disponível)** — validação de formato e dígitos verificadores via algoritmo mod-11.
2. **Cache local (disponível após primeira consulta)** — CPFs previamente consultados ficam armazenados em IndexedDB.
3. **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.

```javascript
// 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

```javascript
// 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.

```javascript
// 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.

```javascript
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.

```javascript
// 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.

```html
<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.

```json
{
 "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.

### Leia também

- [Mobile-first: como otimizar a validação de CPF em dispositivos móveis](https://cpfhub.io/blog/mobile-first-como-otimizar-a-validacao-de-cpf-em-dispositivos-moveis)
- [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)

---

## 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**](https://www.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](https://www.cpfhub.io/) — 50 consultas por mês sem cartão de crédito, sem compromisso.

