NFS-e

Emissor de NFS-e Nacional em Cloudflare Workers, Containers e D1: análise de arquitetura open source vs. API gratuita

Estudo técnico da arquitetura do emissor-nfse-nacional na Cloudflare (Workers, Containers para mTLS com TLS renegotiation, D1 e XMLDSig) e quando usar self-hosted ou a API da Notaas com 20 notas grátis por mês.

Fábio Magalhães CostaAtualizado em 13/11/2026
Arquitetura de emissão de NFS-e Nacional combinando Cloudflare Workers, Containers mTLS e banco D1 SQLite

Arquitetura de emissão de NFS-e Nacional combinando Cloudflare Workers, Containers mTLS e banco D1 SQLite

Há algumas semanas, publiquei aqui uma análise sobre criar um emissor de NFS-e Nacional em Node.js vs. usar uma API gratuita, focando nos tropeços clássicos de rodar processos locais ou containers convencionais com SQLite. Hoje quero analisar uma abordagem de engenharia diferente e muito bem executada que surgiu na comunidade open source brasileira: rodar um emissor de NFS-e Nacional via API diretamente na borda da Cloudflare, combinando Cloudflare Workers (Hono), Cloudflare D1 e Cloudflare Containers.

Ao revisar o código do repositório gabrielbuzziv/emissor-nfse-nacional — onde inclusive abrimos o PR #4 propondo validações prévias para as regras E0010, E0713 e E0247 —, encontrei decisões arquiteturais que valem um estudo técnico detalhado para qualquer desenvolvedor ou CTO avaliando self-hosted na Cloudflare versus uma infraestrutura fiscal gerenciada.

É possível operar um emissor próprio de NFS-e Nacional na Cloudflare usando Workers + D1 + um Container auxiliar para assinar o XML e negociar o mTLS com a SEFIN, a partir do piso de US$ 5/mês do plano Workers Paid; porém, a escolha entre manter essa topologia ou usar as 20 notas gratuitas por mês da Notaas depende do regime tributário, das regras municipais e do apetite da equipe para atualizar schemas como a NT-009.

Uma boa arquitetura open source resolve o transporte e a idempotência com elegância; o desafio de longo prazo no Brasil mora nas exceções tributárias e nas Notas Técnicas.

Por que um Cloudflare Worker puro não fala direto com a SEFIN Nacional?

Quando um desenvolvedor acostumado com a stack da Cloudflare decide integrar com a SEFIN Nacional (sefin.nfse.gov.br) e com o ADN (adn.nfse.gov.br), a primeira tentativa quase sempre é usar o fetch() nativo do runtime workerd ou configurar um binding de mTLS de saída da própria Cloudflare.

Na prática, essa tentativa esbarra em três particularidades da infraestrutura federal da NFS-e:

  1. TLS Renegotiation pós-handshake: Os servidores da NFS-e Nacional solicitam o certificado digital ICP-Brasil (e-CNPJ A1) por meio de renegociação TLS durante a requisição HTTPS, um fluxo que o fetch do Worker não negocia diretamente.
  2. Cifras legadas no arquivo .pfx / .p12: Muitas autoridades certificadoras brasileiras ainda emitem arquivos PKCS#12 cifrados com algoritmos antigos (RC2-40-CBC ou 3DES), exigindo o provider -legacy do OpenSSL 3 para extrair a chave privada e o certificado X.509.
  3. Assinatura XMLDSig (rsa-sha256 + C14N): Antes de comprimir a Declaração de Prestação de Serviço (DPS) em GZip + Base64 (dpsXmlGZipB64), o elemento <infDPS Id="DPS..."> precisa ser canonizado (http://www.w3.org/TR/2001/REC-xml-c14n-20010315) e assinado com a chave privada do emitente.

A solução adotada no emissor-nfse-nacional divide a responsabilidade em duas camadas usando Cloudflare Containers (@cloudflare/containers), mantendo o Worker leve na frente e delegando apenas a criptografia e a rede governamental para um micro-container Node.js sob demanda:

⚡ Arquitetura Híbrida Cloudflare Workers + Container mTLS:

  • 1. API Edge (src/index.ts + Hono): Autentica o Bearer token com comparação em tempo constante (crypto.subtle.timingSafeEqual), valida o payload JSON e coordena o estado da nota no banco Cloudflare D1.
  • 2. Isolamento por Certificado (GovClient Durable Object): Deriva um hash SHA-256 das partes do certificado (gov-${hash}) para rotear chamadas ao container; quando o certificado é renovado, um novo container assume sem precisar reiniciar o antigo manualmente, que hiberna após sleepAfter = '10m'.
  • 3. Fragmentação de Secrets de 5 kB (CERT_PFX_B64_1..N): Como cada secret de Worker tem limite de 5 kB, o script de upload fatia o .pfx em Base64 em múltiplas variáveis (CERT_PFX_B64_1, CERT_PFX_B64_2...) e as recompõe na memória do container.
  • 4. Sidecar de Assinatura e mTLS (container/server.mjs + sign.mjs): Extrai chave e certificado via openssl pkcs12 -legacy, aplica a assinatura XMLDSig sobre o XML já gerado em forma canônica e abre a conexão node:https com pfx e passphrase rumo à SEFIN ou ao ADN.

Veja como o container/sign.mjs resolve a assinatura XMLDSig sem depender de bibliotecas pesadas de C14N. Vale notar que essa simplificação não é um algoritmo geral de C14N para XML arbitrário — ela só funciona porque o serializador próprio do projeto (src/dps.ts) produz um XML estritamente controlado (sem espaços entre tags, sem tags auto-fechadas, com texto escapado e namespace padrão apenas na raiz), e a suíte de testes confere cada assinatura gerada contra o xmlsec1:

import { createHash, createSign } from 'node:crypto'

const DSIG = 'http://www.w3.org/2000/09/xmldsig#'
const NS = 'http://www.sped.fazenda.gov.br/nfse'
const C14N = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'

export const signXml = (xml, keyPem, certB64) => {
  const match = xml.match(/<(inf[A-Za-z]+) Id="([^"]+)">[\s\S]*<\/\1>/)
  if (!match) throw new Error('no element with Id to sign')
  const [element, name, id] = match

  // Atalho específico para o XML controlado emitido pelo serializer do projeto (src/dps.ts):
  // não serve para XML arbitrário, mas evita carregar um parser C14N completo no container:
  const canonical = element.replace(`<${name} Id=`, `<${name} xmlns="${NS}" Id=`)
  const digest = createHash('sha256').update(canonical).digest('base64')

  const signedInfo =
    `<CanonicalizationMethod Algorithm="${C14N}"></CanonicalizationMethod>` +
    `<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></SignatureMethod>` +
    `<Reference URI="#${id}"><Transforms>` +
    `<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></Transform>` +
    `<Transform Algorithm="${C14N}"></Transform></Transforms>` +
    `<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></DigestMethod>` +
    `<DigestValue>${digest}</DigestValue></Reference>`

  const value = createSign('RSA-SHA256')
    .update(`<SignedInfo xmlns="${DSIG}">${signedInfo}</SignedInfo>`)
    .sign(keyPem, 'base64')

  const signature =
    `<Signature xmlns="${DSIG}"><SignedInfo>${signedInfo}</SignedInfo>` +
    `<SignatureValue>${value}</SignatureValue>` +
    `<KeyInfo><X509Data><X509Certificate>${certB64}</X509Certificate></X509Data></KeyInfo></Signature>`

  const rootClose = xml.lastIndexOf('</')
  return xml.slice(0, rootClose) + signature + xml.slice(rootClose)
}

Essa abordagem é enxuta porque evita compilar libxml2 dentro do runtime do Worker e usa xmllint + xmlsec1 apenas na suíte de testes (vitest) para provar que o XML gerado passa nos XSDs oficiais e na verificação criptográfica.

Idempotência no D1, Respostas Perdidas (unknown) e Colisão E0014

Outro ponto bem resolvido em src/notas.ts é a máquina de estados para lidar com timeouts da SEFIN Nacional. Quem opera mensageria fiscal em produção sabe que o pior erro não é uma rejeição síncrona (422), e sim quando a requisição dá timeout de 30 segundos depois que a SEFIN já gravou a nota do lado dela.

No repositório, o banco D1 mantém uma tabela counters (INSERT INTO counters ... ON CONFLICT DO UPDATE SET value = value + 1 RETURNING value) e uma tabela notas com cinco estados (pending, unknown, issued, rejected, canceled):

  1. Antes de chamar a SEFIN: A nota é gravada no D1 com status: 'unknown' e o dpsId de 45 posições (DPS + IBGE 7 dígitos + 2 + CNPJ 14 dígitos + série 5 dígitos + número 15 dígitos), recuando o dhEmi em 60 segundos (CLOCK_SKEW_MS = 60_000) para evitar rejeição E0008 por diferença de relógio com o servidor do governo.
  2. Se a conexão cair ou a SEFIN retornar 5xx: A nota permanece em unknown. Na próxima chamada com o mesmo id da venda, o emissor consulta GET /SefinNacional/dps/{dpsId} antes de tentar retransmitir.
  3. Se o número da DPS já tiver sido usado no portal web (E0014): Quando o empreendedor emite uma nota avulsa pelo portal gov.br usando a mesma série, o contador local colide e recebe o erro E0014 (duplicidade de DPS). O método findIssued baixa o XML daquela DPS e confere se o valor (vServ) e o documento do tomador batem com o pedido; se pertencerem a outra venda, ele reserva o próximo dpsNumber (até 5 colisões) e reenvia automaticamente.

O que propusemos no PR #4: validações prévias para as regras E0010, E0713 e E0247

Ao cruzar a implementação do projeto com as baterias de validação que rodamos na Notaas para validar e tratar erros na API da NFS-e, analisamos três lacunas que o próprio autor havia mapeado em docs/conformidade.md e na Issue #2 do repositório. Abrimos o Pull Request #4 propondo validar esses três pontos na entrada antes que a nota consuma numeração de DPS ou bata na SEFIN:

/** Rule E0010 reserves series 1 to 49999 for the API. */
const isApiSeries = (value: string): boolean =>
  /^[1-9]\d{0,4}$/.test(value) && Number(value) <= 49999

/** TSEmail in the layout (1 to 80 characters) and rule E0247. */
const EMAIL =
  /^[a-zA-Z0-9_%+-]+(?:\.[a-zA-Z0-9_%+-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/
  • Regra E0010 (DPS_SERIES de 1 a 49999): O schema XSD aceita até 5 dígitos (TSSerieDPS), mas a regra de negócio E0010 do Anexo I reserva à integração via API exclusivamente a faixa de 1 a 49999 (séries 50000+ pertencem a outros emissores públicos). No PR, propusemos restringir configProblems para acusar erro caso a série seja 0, tenha zeros à esquerda ou ultrapasse 49999.
  • Regra E0713 (ISSUER_SIMPLES_OPTION para Não Optantes): O emissor envia <indTotTrib>0</indTotTrib> para qualquer CNPJ que não seja ME/EPP (opSimpNac !== '3'). Para o MEI (opSimpNac = '2'), a regra E0710 exige exatamente indTotTrib = 0. Porém, para empresas fora do Simples (opSimpNac = '1', Lucro Presumido/Real), a regra E0713 proíbe tanto indTotTrib quanto pTotTribSN, exigindo a abertura de tributos federais, estaduais e municipais (vTotTrib ou pTotTrib). Enquanto o projeto não implementa esse grupo, o PR propõe que configProblems aceite apenas 2 (MEI) ou 3 (ME/EPP), sinalizando o problema no /health (503) e impedindo a chamada de emissão (500 em configFrom) antes de gerar uma DPS que seria recusada pela SEFIN.
  • Regra E0247 (taker.email alinhado ao TSEmail): Se o cliente digitasse um e-mail com espaço, domínio malformado ou mais de 80 caracteres no checkout, a DPS era assinada e recusada pela SEFIN com erro E0247. No PR, propusemos extrair parseIssueRequest para src/notas.ts com trim(), trava de 80 caracteres e validação básica de formato, devolvendo HTTP 400 imediato.

O gargalo do PDF: pdf-lib minimalista vs. DANFSe v2.0 (NT-008) com @notaas/danfse-viewer

Todo desenvolvedor que integra com o Sistema Nacional descobre rápido que o endpoint oficial de PDF do governo (GET https://adn.nfse.gov.br/danfse/{chave}) oscila com frequência, retornando 502/503 nos horários de pico — o que motiva o fim da dependência do visualizador do ADN para gerar o DANFSe local.

No emissor-nfse-nacional, quando o ADN não responde, o arquivo src/danfse.ts entra em ação desenhando um PDF substituto diretamente no Worker com pdf-lib e uqr (para o QR Code da consulta pública). Como solução de contingência dentro de um Worker sem navegador headless, é uma saída inteligente. Contudo, vale observar a diferença de escopo documental:

  • O PDF desenhado em src/danfse.ts é uma representação simplificada de meia página pensada apenas para o Simples Nacional sem retenções, omitindo blocos do layout oficial da DANFSe v2.0 especificada na NT-008 (como retenções federais PIS/COFINS/IRRF/CSLL/INSS, deduções, intermediário, construção civil e o quadro de destaque do IBS/CBS da Reforma Tributária).
  • Quando o seu cliente é uma empresa maior ou um setor de contas a pagar/contabilidade que exige o espelho fiel da NT-008 v2.0, você pode utilizar nossa biblioteca open source MIT @notaas/danfse-viewer no GitHub (ou testar gratuitamente no playground danfse.notaas.com.br), que faz o parse completo do XML v1.01/v1.02 e gera o documento no layout oficial.

A matemática real: quando rodar self-hosted na Cloudflare e quando usar a API da Notaas?

O que mais gosto na documentação do gabrielbuzziv/emissor-nfse-nacional é a transparência sobre para quem o projeto serve e onde estão as suas fronteiras. Colocar os números e o escopo fiscal lado a lado ajuda a tomar uma decisão racional de arquitetura:

Critério Técnico & Operacional emissor-nfse-nacional (Self-Hosted Cloudflare) Notaas API (Infraestrutura Fiscal Gerenciada)
Custo Mensal Inicial Piso de US$ 5/mês (plano Cloudflare Workers Paid exigido pelo Containers + franquia lite de ~100h/mês e D1; excedentes de CPU/memória/banco tarifados à parte). R$ 0/mês para até 20 notas/mês em produção real (1 CNPJ) + até 5 projetos em homologação via Org API (sem cartão de crédito). Planos pagos a partir de R$ 99/mês (500 notas).
Regimes Tributários Atendidos Apenas Simples Nacional (ME, EPP e MEI) sem retenção de ISSQN e sem destaque de tributos federais. Não atende Lucro Presumido ou Lucro Real (E0713). Todos os regimes: MEI, Simples Nacional (anexos com ou sem retenção, sublimite), Lucro Presumido, Lucro Real, Imunes/Isentas e Pessoa Física (CPF/CAEPF).
Municípios e Roteamento Híbrido Fala apenas com a API Nacional (sefin.nfse.gov.br). Estar conveniado ao padrão nacional não significa usar os emissores nacionais (produtos disponíveis gov.br): segundo o monitoramento oficial de adesões (gov.br/nfse, snapshot de 10/11/2026 documentado no repositório), 3.495 municípios adotam os emissores públicos nacionais e 2.050 mantêm sistema próprio (AderenteEmissorNacional = Não, onde a regra E0039 recusa a DPS enviada à API nacional fora das exceções do MEI e da Resolução CGSN 191/2026). 4.836 municípios ativos (snapshot interno Notaas de nov/2026) com roteamento automático entre o Padrão Nacional (SNNFSE) e dezenas de sistemas municipais próprios (ABRASF, GINFES, IPM, GovDigital, ISSNet, etc.) conforme o regime e o município.
Reforma Tributária (NT-009 / IBS e CBS) Em Restricted já envia finNFSe=0 e reposiciona cNBS, mas ainda não monta o grupo <IBSCBS> nem os novos campos do Simples da NT-009 (regApIBSCBSSN e cAtvSN). Exige atualização manual do src/dps.ts conforme o cronograma de 2027 (Ato Conjunto RFB/CGIBS nº 4/2026). Suporte nativo ao schema DPS com IBS e CBS e regras da NT-009, correlação automática do Anexo VIII e preenchimento padrão por projeto.
Gestão Multi-CNPJ & Certificados 1 CNPJ por deploy (configurado nas vars do wrangler.jsonc e até 3 instâncias no container). Exige certificado e-CNPJ A1 .pfx (inclusive para MEI). Org API Multi-CNPJ (POST /api/v1/org/projects) para isolar dezenas ou centenas de clientes, com suporte a Procuração Eletrônica RFB (00233) no ambiente nacional sem exigir .pfx do MEI.

Quando o projeto open source na Cloudflare é uma ótima escolha?

Se você tem uma única empresa do Simples Nacional (ME ou EPP), presta um serviço padronizado tributado no próprio município sem retenção de ISS, já paga o plano Workers Paid da Cloudflare por outros projetos, emite mais de 50 ou 100 notas por mês nesse único CNPJ e gosta de controlar seu próprio código acompanhando as Notas Técnicas no GitHub, o emissor-nfse-nacional é um dos projetos mais limpos e bem documentados disponíveis hoje.

Quando faz mais sentido usar a API da Notaas?

Por outro lado, existem três cenários em que manter a infraestrutura por conta própria deixa de compensar financeiramente e operacionalmente:

  1. Você está validando um SaaS ou emite até 20 notas por mês: Na Cloudflare, você precisa assinar o plano Workers Paid (piso de US$ 5/mês em dólar) para ter acesso ao runtime de Containers. Na Notaas, você emite até 20 notas fiscais por mês em produção real por R$ 0/mês, sem precisar subir container Docker, fatiar certificado .pfx em variáveis de ambiente nem configurar banco D1.
  2. Você atende múltiplos CNPJs, clientes fora do Simples ou prefeituras com sistema próprio (AderenteEmissorNacional = Não): Assim que entra um cliente no Lucro Presumido, uma nota com retenção de ISS/IRRF na fonte, um serviço de construção civil/eventos ou um município com sistema próprio fora das exceções do MEI e da CGSN 191/2026, uma integração ligada apenas ao endpoint básico da SEFIN Nacional esbarra nas rejeições E0039, E0370, E0390 e E0713.
  3. Você não quer reescrever XML na virada de 2027 (NT-009 e IBS/CBS): A transição do leiaute da NT-009 da produção restrita para a produção e a entrada das regras de IBS/CBS em janeiro de 2027 exigem atualização contínua de schemas XSD e regras de validação. Na Notaas, você envia apenas um JSON simples para POST https://api.notaas.com.br/api/v1/emitir e nossa engenharia absorve todas as mudanças de leiaute no motor:
const response = await fetch('https://api.notaas.com.br/api/v1/emitir', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.NOTAAS_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    tipo: 'NFSE',
    externalId: 'pedido:1001',
    tomador: {
      cpfCnpj: '12345678909',
      razaoSocial: 'Maria Silva',
      email: 'maria@example.com',
    },
    servico: {
      valorServicos: 97.00,
      itemListaServico: '080201',
      codigoNbs: '122051900',
      discriminacao: 'Curso online',
    },
  }),
});

Seja contribuindo com código aberto em repositórios da comunidade ou liberando um plano gratuito real em produção, nossa visão na Notaas é a mesma: desenvolvedor brasileiro deveria gastar energia construindo o produto principal do seu negócio, e não decifrando erro de renegociação mTLS ou planilha de rejeição da SEFIN.

Notaas — Infraestrutura Fiscal Multi-Tenant para SaaS, ERPs e BPOs

Escale a emissão de NFS-e, NF-e, NFC-e, CT-e e MDF-e para dezenas de CNPJs

Integre a API de Nota Fiscal da Notaas no seu software: provisione clientes via Org API Multi-CNPJ, isole certificados A1 em nuvem, receba webhooks assinados com HMAC e gere DANFSe v2.0 local. Sandbox ilimitado e planos sob medida para operações em escala.

Artigos Relacionados em NFS-e

Guias Pilares de Integração Fiscal