Pular para o conteúdo
📖 COOKBOOK

CPF/CNPJ com validação e Receita Federal.

Aceita CPF/CNPJ no signup, valida dígitos verificadores localmente, faz lookup na Receita pra CNPJ (razão social, situação, endereço), persiste no perfil. Único do SuperDB.

O que vamos fazer

  • Input único que aceita CPF ou CNPJ (detecta pelo número de dígitos)
  • Mask automática (000.000.000-00 ou 00.000.000/0000-00)
  • Validação client-side dos dígitos verificadores (sem chamada de rede)
  • Pra CNPJ, lookup na Receita Federal: razão social, situação cadastral, endereço, CNAE
  • Persistência segura: o SuperDB cifra CPF em auth.users.encrypted_pii automaticamente

Pré-requisitos

  • SuperDB Free serve (a feature de PII encryption está em todos os planos desde Sprint 5)
  • @superdb/supabase-compat ≥ 0.4 (ou @superdb/auth-js ≥ 0.4)
  • Plano Pro+ pra usar o endpoint db.auth.lookupCNPJ() (Free pode usar API pública da Receita, mas com rate limit baixo)

Passo 1 — Validação client (dígitos verificadores)

O algoritmo dos DVs é determinístico — não precisa de rede. Cole essas duas funções num lib/cpf-cnpj.ts:

lib/cpf-cnpj.ts
export function validateCPF(value: string): boolean {
  const cpf = value.replace(/\D/g, '')
  if (cpf.length !== 11) return false
  if (/^(\d)\1{10}$/.test(cpf)) return false  // 111.111.111-11 etc

  const calc = (slice: string, factor: number) => {
    let sum = 0
    for (const d of slice) sum += parseInt(d) * factor--
    const r = (sum * 10) % 11
    return r === 10 ? 0 : r
  }
  return calc(cpf.slice(0, 9), 10) === parseInt(cpf[9])
      && calc(cpf.slice(0, 10), 11) === parseInt(cpf[10])
}

export function validateCNPJ(value: string): boolean {
  const cnpj = value.replace(/\D/g, '')
  if (cnpj.length !== 14) return false
  if (/^(\d)\1{13}$/.test(cnpj)) return false

  const calc = (slice: string, factors: number[]) => {
    let sum = 0
    for (let i = 0; i < slice.length; i++) sum += parseInt(slice[i]) * factors[i]
    const r = sum % 11
    return r < 2 ? 0 : 11 - r
  }
  const f1 = [5,4,3,2,9,8,7,6,5,4,3,2]
  const f2 = [6,5,4,3,2,9,8,7,6,5,4,3,2]
  return calc(cnpj.slice(0, 12), f1) === parseInt(cnpj[12])
      && calc(cnpj.slice(0, 13), f2) === parseInt(cnpj[13])
}

export function maskBR(value: string) {
  const d = value.replace(/\D/g, '')
  if (d.length <= 11) {
    // CPF: 000.000.000-00
    return d
      .replace(/(\d{3})(\d)/, '$1.$2')
      .replace(/(\d{3})(\d)/, '$1.$2')
      .replace(/(\d{3})(\d)/, '$1-$2')
  }
  // CNPJ: 00.000.000/0000-00
  return d
    .replace(/(\d{2})(\d)/, '$1.$2')
    .replace(/(\d{3})(\d)/, '$1.$2')
    .replace(/(\d{3})(\d)/, '$1/$2')
    .replace(/(\d{4})(\d)/, '$1-$2')
}

Passo 2 — Formulário de signup

Input único, valida ao perder foco, mostra check verde:

app/signup/identidade.tsx
'use client'
import { useState } from 'react'
import { validateCPF, validateCNPJ, maskBR } from '@/lib/cpf-cnpj'

export function IdentidadeInput() {
  const [valor, setValor] = useState('')
  const [erro, setErro] = useState<string | null>(null)
  const [ok, setOk] = useState(false)

  function onBlur() {
    const digits = valor.replace(/\D/g, '')
    if (digits.length === 11) {
      if (!validateCPF(valor)) return setErro('CPF inválido')
    } else if (digits.length === 14) {
      if (!validateCNPJ(valor)) return setErro('CNPJ inválido')
    } else {
      return setErro('Digite 11 dígitos (CPF) ou 14 (CNPJ)')
    }
    setErro(null)
    setOk(true)
  }

  return (
    <div>
      <input
        value={valor}
        onChange={(e) => { setValor(maskBR(e.target.value)); setOk(false) }}
        onBlur={onBlur}
        placeholder="CPF ou CNPJ"
      />
      {ok && <span>✓</span>}
      {erro && <span style={{color: 'red'}}>{erro}</span>}
    </div>
  )
}

Passo 3 — Server revalida e persiste

Nunca confie só na validação client. O SuperDB já tem validação no signUp — basta passar o CPF em data:

app/signup/action.ts
'use server'
import { createServerClient } from '@superdb/supabase-compat/ssr'
import { cookies } from 'next/headers'
import { validateCPF } from '@/lib/cpf-cnpj'

export async function signupAction(formData: FormData) {
  const email = formData.get('email') as string
  const cpf = (formData.get('cpf') as string).replace(/\D/g, '')

  // Server revalida — defesa em profundidade
  if (!validateCPF(cpf)) {
    return { erro: 'CPF inválido' }
  }

  const cookieStore = await cookies()
  const db = createServerClient(/* ... */)

  const { error } = await db.auth.signUp({
    email,
    password: formData.get('password') as string,
    options: {
      data: { cpf },  // SuperDB cifra automaticamente
    },
  })

  return error ? { erro: error.message } : { ok: true }
}
💡

Dica: o CPF/CNPJ entra em raw_user_meta_data, mas o SuperDB detecta o campo e cifra em encrypted_pii (AES-256-GCM) automaticamente. Quando você ler via db.auth.getUser(), ele decifra na hora pro user autenticado. Outros users não conseguem ver mesmo com SQL direto — a chave fica fora do banco.

Passo 4 — Lookup CNPJ na Receita

Quando o input tem 14 dígitos válidos, busque dados públicos da empresa. Útil pra pré-preencher razão social, validar se a empresa está ativa, mostrar endereço:

app/signup/lookup-cnpj.ts
import { createBrowserClient } from '@superdb/supabase-compat'

const db = createBrowserClient(
  process.env.NEXT_PUBLIC_SUPERDB_URL!,
  process.env.NEXT_PUBLIC_SUPERDB_ANON_KEY!
)

const { data, error } = await db.auth.lookupCNPJ('00.000.000/0001-91')

// data = {
//   razao_social: 'BANCO DO BRASIL SA',
//   nome_fantasia: 'BB',
//   situacao: 'ATIVA',
//   data_situacao: '2005-11-03',
//   endereco: { logradouro, numero, bairro, municipio, uf, cep },
//   atividade_principal: { codigo: '6422100', descricao: 'Bancos múltiplos...' },
//   cached_at: '2026-05-11T14:00:00Z',  // cache 24h
// }
app/signup/lookup-cnpj.js
const { data, error } = await db.auth.lookupCNPJ('00.000.000/0001-91')
console.log(data.razao_social)  // 'BANCO DO BRASIL SA'
terminal
curl "$SUPERDB_URL/auth/v1/cnpj/00000000000191" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $USER_JWT"

Use no form pra pré-preencher campos depois do lookup:

app/signup/empresa-form.tsx
async function buscarReceita() {
  setBuscando(true)
  const { data, error } = await db.auth.lookupCNPJ(cnpj)
  setBuscando(false)
  if (error) return setErro('CNPJ não encontrado na Receita')

  setRazaoSocial(data.razao_social)
  setEndereco(data.endereco.logradouro + ', ' + data.endereco.numero)
  setCidade(data.endereco.municipio)
  setUf(data.endereco.uf)

  if (data.situacao !== 'ATIVA') {
    setAviso(`Atenção: empresa em situação ${data.situacao}`)
  }
}

Resultado

Signup com identidade brasileira validada localmente em milissegundos + lookup completo da empresa em ~400ms (cache 24h). Reduz fraude e melhora UX porque user não precisa digitar razão social.

meu-app.com/signup
v0.4.2
CNPJ DA EMPRESA
00.000.000/0001-91
RAZÃO SOCIAL · auto-preenchido
BANCO DO BRASIL SA
SITUAÇÃO
ATIVA

Variações

Apenas CPF (B2C)

Se seu produto é só pra pessoa física, simplifique pra um input fixo de 11 dígitos:

app/signup/cpf-only.tsx
const digits = valor.replace(/\D/g, '')
if (digits.length !== 11 || !validateCPF(digits)) {
  return setErro('CPF inválido')
}

Apenas CNPJ (B2B)

SaaS pra empresas: aceite só CNPJ e force o lookup na Receita como parte do onboarding. Bloqueia signups fraudulentos.

Radio button PF / PJ

Mostra ao usuário a escolha explícita antes de pedir o documento. UX mais limpa que detectar automaticamente:

app/signup/pf-pj-radio.tsx
<label><input type="radio" value="pf" /> Pessoa Física</label>
<label><input type="radio" value="pj" /> Pessoa Jurídica</label>

{tipo === 'pf' && <CpfInput />}
{tipo === 'pj' && <CnpjInput onLookup={preencherEmpresa} />}

Erros comuns

CPFs de teste passam no algoritmo

123.456.789-09 e outros "famosos" passam nos dígitos verificadores. Bloqueie via blacklist explícita pra evitar signup de fraude:

lib/cpf-blacklist.ts
const CPFS_BLOQUEADOS = new Set([
  '12345678909',
  '98765432100',
  '11122233344',
  // ... popule com os ~50 CPFs mais usados em testes
])

export function isCpfReal(cpf: string) {
  return validateCPF(cpf) && !CPFS_BLOQUEADOS.has(cpf.replace(/\D/g,''))
}

CNPJ inativo na Receita

Empresa baixada, suspensa ou inapta retorna situacao !== 'ATIVA'. Decida regra: alertar (B2C com checkout) ou bloquear (B2B com contratos). Não ignore — empresa inativa não emite NF-e.

⚠️

Cuidado: a API de lookup tem rate limit (10 req/min por usuário autenticado, 100/min por projeto). Em telas de "validar em tempo real" enquanto digita, debouncing é obrigatório — espere o user terminar de digitar 14 dígitos válidos antes de chamar. Senão, vai esgotar o limite em 1 user que está apagando e digitando.

Dados cacheados desatualizados

O cache da Receita é de 24h. Em casos raros (mudança de razão social, baixa recente), os dados podem estar desatualizados. Solução: ofereça botão "atualizar dados da Receita" que força refresh — chame db.auth.lookupCNPJ(cnpj, { force: true }).

ℹ️

Sobre LGPD: CPF é dado pessoal sensível. O SuperDB cifra em repouso (AES-256-GCM) e mascara em logs automaticamente. CNPJ é dado público (consultável na Receita) e não tem o mesmo tratamento — fica em claro. Configure o que é PII em Auth → PII Fields se quiser cifrar campos próprios também.

Essa página ajudou?