Como criar um campo de CPF que funcione bem em modo offline

Aprenda a criar um campo de CPF funcional em modo offline com Service Workers, validação local e sincronização posterior.

Redação CPFHub.io
Redação CPFHub.io
··10 min de leitura
Como criar um campo de CPF que funcione bem em modo offline

Um campo de CPF offline-first valida o formato localmente sem precisar de internet, armazena resultados anteriores via IndexedDB e enfileira CPFs coletados para validar com a API da CPFHub.io assim que a conexão retorna — garantindo que o fluxo de trabalho nunca seja interrompido por instabilidade de rede.

Introdução

Nem todo usuário tem conexão estável o tempo todo. Representantes comerciais em campo, equipes de atendimento em áreas rurais, eventos presenciais com Wi-Fi instável -- são muitos os cenários onde um formulário com CPF precisa funcionar offline ou com conexão intermitente. Um campo de CPF que trava ou exibe erros quando não há internet frustra o usuário e interrompe o fluxo de trabalho.


Arquitetura offline-first para CPF

A abordagem offline-first significa que a aplicação é projetada para funcionar sem conexão como estado padrão, com a conectividade sendo um recurso adicional. Para validação de CPF, isso implica:

  1. Validação local de formato: sempre disponível, sem dependência de rede.
  2. Cache de consultas anteriores: resultados de API armazenados localmente.
  3. Fila de sincronização: CPFs coletados offline são validados quando a conexão retorna.
  4. Indicador de estado: o usuário sabe se a validação foi local ou via API.

Detector de conexão

Primeiro, crie um módulo que monitora o estado da conexão:

var ConnectionMonitor = (function () {
    var isOnline = navigator.onLine;
    var listeners = [];

    window.addEventListener("online", function () {
    isOnline = true;
    notifyListeners();
    });

    window.addEventListener("offline", function () {
    isOnline = false;
    notifyListeners();
    });

    function notifyListeners() {
    listeners.forEach(function (fn) {
    fn(isOnline);
    });
    }

    function onChange(fn) {
    listeners.push(fn);
    }

    function getStatus() {
    return isOnline;
    }

    return { onChange: onChange, getStatus: getStatus };
})();

Validação local robusta

A validação local é a base da funcionalidade offline. Ela valida o formato sem depender de rede:

var CpfValidator = {
    validateFormat: function (cpf) {
    var digits = cpf.replace(/\D/g, "");

    if (digits.length !== 11) {
    return { valid: false, reason: "incomplete" };
    }

    if (/^(\d)\1{10}$/.test(digits)) {
    return { valid: false, reason: "repeated" };
    }

    // Primeiro dígito verificador
    var sum = 0;
    for (var i = 0; i < 9; i++) {
    sum += parseInt(digits[i]) * (10 - i);
    }
    var r1 = (sum * 10) % 11;
    if (r1 === 10) r1 = 0;
    if (r1 !== parseInt(digits[9])) {
    return { valid: false, reason: "check_digit_1" };
    }

    // Segundo dígito verificador
    sum = 0;
    for (var i = 0; i < 10; i++) {
    sum += parseInt(digits[i]) * (11 - i);
    }
    var r2 = (sum * 10) % 11;
    if (r2 === 10) r2 = 0;
    if (r2 !== parseInt(digits[10])) {
    return { valid: false, reason: "check_digit_2" };
    }

    return { valid: true, reason: null };
    },
};

Cache local com IndexedDB

O IndexedDB oferece armazenamento persistente e estruturado, ideal para cache de consultas de CPF:

var CpfCache = (function () {
    var DB_NAME = "cpfhub_cache";
    var STORE_NAME = "cpf_results";
    var DB_VERSION = 1;
    var CACHE_TTL = 24 * 60 * 60 * 1000; // 24 horas

    function openDB() {
    return new Promise(function (resolve, reject) {
    var request = indexedDB.open(DB_NAME, DB_VERSION);

    request.onupgradeneeded = function (event) {
    var db = event.target.result;
    if (!db.objectStoreNames.contains(STORE_NAME)) {
    var store = db.createObjectStore(STORE_NAME, { keyPath: "cpf" });
    store.createIndex("timestamp", "timestamp");
    }
    };

    request.onsuccess = function () {
    resolve(request.result);
    };

    request.onerror = function () {
    reject(request.error);
    };
    });
    }

    async function get(cpf) {
    var db = await openDB();
    return new Promise(function (resolve) {
    var tx = db.transaction(STORE_NAME, "readonly");
    var store = tx.objectStore(STORE_NAME);
    var req = store.get(cpf);

    req.onsuccess = function () {
    var result = req.result;
    if (result && Date.now() - result.timestamp < CACHE_TTL) {
    resolve(result.data);
    } else {
    resolve(null);
    }
    };

    req.onerror = function () {
    resolve(null);
    };
    });
    }

    async function set(cpf, data) {
    var db = await openDB();
    return new Promise(function (resolve) {
    var tx = db.transaction(STORE_NAME, "readwrite");
    var store = tx.objectStore(STORE_NAME);
    store.put({
    cpf: cpf,
    data: data,
    timestamp: Date.now(),
    });
    tx.oncomplete = function () {
    resolve();
    };
    });
    }

    async function cleanup() {
    var db = await openDB();
    var tx = db.transaction(STORE_NAME, "readwrite");
    var store = tx.objectStore(STORE_NAME);
    var index = store.index("timestamp");
    var cutoff = Date.now() - CACHE_TTL;

    var req = index.openCursor(IDBKeyRange.upperBound(cutoff));
    req.onsuccess = function (event) {
    var cursor = event.target.result;
    if (cursor) {
    cursor.delete();
    cursor.continue();
    }
    };
    }

    return { get: get, set: set, cleanup: cleanup };
})();

Fila de sincronização offline

CPFs coletados offline ficam em uma fila para validação quando a conexão retornar:

var SyncQueue = (function () {
    var QUEUE_KEY = "cpfhub_sync_queue";

    function getQueue() {
    try {
    return JSON.parse(localStorage.getItem(QUEUE_KEY) || "[]");
    } catch (e) {
    return [];
    }
    }

    function saveQueue(queue) {
    localStorage.setItem(QUEUE_KEY, JSON.stringify(queue));
    }

    function add(cpf, formData) {
    var queue = getQueue();
    queue.push({
    cpf: cpf,
    formData: formData,
    timestamp: Date.now(),
    status: "pending",
    });
    saveQueue(queue);
    }

    async function processQueue() {
    var queue = getQueue();
    var pending = queue.filter(function (item) {
    return item.status === "pending";
    });

    for (var i = 0; i < pending.length; i++) {
    var item = pending[i];

    var controller = new AbortController();
    var timeoutId = setTimeout(function () {
    controller.abort();
    }, 10000);

    try {
    var 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);
    var data = await res.json();

    item.status = data.success ? "validated" : "invalid";
    item.apiData = data;

    // Cachear resultado
    if (data.success) {
    await CpfCache.set(item.cpf, data);
    }
    } catch (err) {
    clearTimeout(timeoutId);
    // Manter como pendente para nova tentativa
    break;
    }
    }

    saveQueue(queue);
    return queue;
    }

    function getPendingCount() {
    return getQueue().filter(function (i) {
    return i.status === "pending";
    }).length;
    }

    return { add: add, processQueue: processQueue, getPendingCount: getPendingCount };
})();

Componente do campo de CPF offline-first

<div class="cpf-offline" id="cpf-field">
    <div class="cpf-offline__status" id="connection-status">
    <span class="status-dot"></span>
    <span class="status-text"></span>
    </div>

    <div class="form-group">
    <label for="cpf">CPF</label>
    <input
    type="text"
    id="cpf"
    class="cpf-input"
    inputmode="numeric"
    maxlength="14"
    placeholder="000.000.000-00"
    />
    <div class="cpf-feedback" id="cpf-feedback" role="status" aria-live="polite"></div>
    </div>

    <div class="sync-info" id="sync-info"></div>
</div>
.cpf-offline__status {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    margin-bottom: 12px;
}

.status-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    transition: background-color 0.3s ease;
}

.status-dot--online {
    background: #059669;
}

.status-dot--offline {
    background: #dc2626;
}

.status-text--online {
    color: #059669;
}

.status-text--offline {
    color: #dc2626;
}

.cpf-feedback {
    font-size: 13px;
    margin-top: 6px;
    min-height: 20px;
}

.cpf-feedback--local {
    color: #f59e0b;
}

.cpf-feedback--api {
    color: #059669;
}

.cpf-feedback--cached {
    color: #3b82f6;
}

.cpf-feedback--error {
    color: #dc2626;
}

.sync-info {
    font-size: 12px;
    color: #64748b;
    margin-top: 8px;
    padding: 8px 12px;
    background: #f8fafc;
    border-radius: 6px;
    display: none;
}

.sync-info--visible {
    display: block;
}

JavaScript principal

var cpfInput = document.getElementById("cpf");
var feedback = document.getElementById("cpf-feedback");
var statusDot = document.querySelector(".status-dot");
var statusText = document.querySelector(".status-text");
var syncInfo = document.getElementById("sync-info");
var debounceTimer = null;

// Atualizar indicador de conexão
function updateConnectionUI(online) {
    statusDot.className = "status-dot status-dot--" + (online ? "online" : "offline");
    statusText.className = "status-text status-text--" + (online ? "online" : "offline");
    statusText.textContent = online ? "Online" : "Offline";
}

updateConnectionUI(ConnectionMonitor.getStatus());
ConnectionMonitor.onChange(function (online) {
    updateConnectionUI(online);
    if (online) {
    syncPending();
    }
});

// Input handler
cpfInput.addEventListener("input", function () {
    var v = this.value.replace(/\D/g, "").slice(0, 11);
    if (v.length > 9)
    v = v.replace(/(\d{3})(\d{3})(\d{3})(\d{1,2})/, "$1.$2.$3-$4");
    else if (v.length > 6)
    v = v.replace(/(\d{3})(\d{3})(\d{1,3})/, "$1.$2.$3");
    else if (v.length > 3) v = v.replace(/(\d{3})(\d{1,3})/, "$1.$2");
    this.value = v;

    var digits = v.replace(/\D/g, "");

    if (digits.length < 11) {
    feedback.textContent = "";
    return;
    }

    if (debounceTimer) clearTimeout(debounceTimer);
    debounceTimer = setTimeout(function () {
    validateCpf(digits);
    }, 400);
});

async function validateCpf(cpf) {
    // 1. Validação local
    var localResult = CpfValidator.validateFormat(cpf);
    if (!localResult.valid) {
    feedback.textContent = "CPF com formato inválido.";
    feedback.className = "cpf-feedback cpf-feedback--error";
    return;
    }

    // 2. Verificar cache
    var cached = await CpfCache.get(cpf);
    if (cached && cached.success) {
    feedback.textContent = "CPF válido (cache) - " + cached.data.name;
    feedback.className = "cpf-feedback cpf-feedback--cached";
    return;
    }

    // 3. Se online, consultar API
    if (ConnectionMonitor.getStatus()) {
    feedback.textContent = "Validando online...";
    feedback.className = "cpf-feedback";

    var controller = new AbortController();
    var timeoutId = setTimeout(function () {
    controller.abort();
    }, 10000);

    try {
    var res = await fetch("https://api.cpfhub.io/cpf/" + cpf, {
    headers: {
    "x-api-key": "SUA_CHAVE_DE_API",
    Accept: "application/json",
    },
    signal: controller.signal,
    });
    clearTimeout(timeoutId);
    var data = await res.json();

    if (data.success) {
    feedback.textContent = "CPF válido - " + data.data.name;
    feedback.className = "cpf-feedback cpf-feedback--api";
    await CpfCache.set(cpf, data);
    } else {
    feedback.textContent = "CPF não encontrado.";
    feedback.className = "cpf-feedback cpf-feedback--error";
    }
    return;
    } catch (err) {
    clearTimeout(timeoutId);
    // Falha de rede -- tratar como offline
    }
    }

    // 4. Modo offline: validação local + fila
    feedback.textContent =
    "CPF com formato válido (verificação offline). Será validado quando a conexão retornar.";
    feedback.className = "cpf-feedback cpf-feedback--local";

    SyncQueue.add(cpf, {});
    updateSyncInfo();
}

function updateSyncInfo() {
    var count = SyncQueue.getPendingCount();
    if (count > 0) {
    syncInfo.textContent =
    count +
    (count === 1
    ? " CPF aguardando validação online."
    : " CPFs aguardando validação online.");
    syncInfo.className = "sync-info sync-info--visible";
    } else {
    syncInfo.className = "sync-info";
    }
}

async function syncPending() {
    var pending = SyncQueue.getPendingCount();
    if (pending === 0) return;

    syncInfo.textContent = "Sincronizando " + pending + " CPF(s)...";
    await SyncQueue.processQueue();
    updateSyncInfo();
}

// Limpar cache antigo periodicamente
CpfCache.cleanup();

Service Worker para cache de assets

Para que a página inteira funcione offline, registre um Service Worker:

// sw.js
var CACHE_NAME = "cpf-form-v1";
var ASSETS = [
    "/",
    "/index.html",
    "/css/style.css",
    "/js/app.js",
];

self.addEventListener("install", function (event) {
    event.waitUntil(
    caches.open(CACHE_NAME).then(function (cache) {
    return cache.addAll(ASSETS);
    })
    );
});

self.addEventListener("fetch", function (event) {
    // Não cachear chamadas de API
    if (event.request.url.includes("api.cpfhub.io")) {
    return;
    }

    event.respondWith(
    caches.match(event.request).then(function (response) {
    return response || fetch(event.request);
    })
    );
});
// Registro no HTML principal
if ("serviceWorker" in navigator) {
    navigator.serviceWorker.register("/sw.js");
}

Perguntas frequentes

O que é necessário para implementar validação de CPF neste contexto?

A validação de CPF exige uma chamada à API com o número do documento e a chave de autenticação. A CPFHub.io retorna o status do CPF, nome do titular e data de nascimento em menos de 200ms, permitindo a verificação em tempo real durante o cadastro ou transação.

A API CPFHub.io funciona para todos os volumes de consulta?

Sim. O plano gratuito oferece 50 consultas por mês sem cartão de crédito — ideal para testes e projetos pequenos. Para volumes maiores, o plano Pro inclui 1.000 consultas mensais por R$149. Se o limite for ultrapassado, a API não bloqueia: cobra R$0,15 por consulta adicional.

Como garantir conformidade com a LGPD ao usar uma API de CPF?

Use o CPF apenas para a finalidade declarada ao titular, armazene apenas o necessário (não guarde o CPF cru se um token bastar), implemente controle de acesso aos logs de consulta e documente a base legal para o tratamento. A ANPD orienta que dados de identificação devem ser tratados com o princípio da necessidade.

Quanto tempo leva para integrar a API CPFHub.io?

A integração básica leva menos de 30 minutos: crie uma conta em cpfhub.io, gere a API key no painel e faça uma chamada GET para https://api.cpfhub.io/cpf/{CPF} com o header x-api-key. A documentação inclui exemplos em Python, Node.js, PHP, Java e outras linguagens.


Conclusão

Um campo de CPF offline-first garante que a coleta de dados nunca seja interrompida por problemas de conexão. Com validação local de formato, cache via IndexedDB, fila de sincronização e indicadores claros de estado, o usuário sabe exatamente o que esperar em cada situação. Quando a conexão está disponível, a API do CPFHub.io confirma os dados cadastrais completos — nome, gênero e data de nascimento — em cerca de 900ms.

A API da CPFHub.io oferece respostas em ~900ms, uptime de 99,9% e conformidade com a LGPD. O plano gratuito com 50 consultas mensais é ideal para equipes que trabalham em campo e precisam validar CPFs de forma intermitente.

Cadastre-se em cpfhub.io — 50 consultas mensais gratuitas, sem cartão de crédito — e comece hoje mesmo.

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