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:
- 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
fetchdo Worker não negocia diretamente. - Cifras legadas no arquivo
.pfx/.p12: Muitas autoridades certificadoras brasileiras ainda emitem arquivos PKCS#12 cifrados com algoritmos antigos (RC2-40-CBCou3DES), exigindo o provider-legacydo OpenSSL 3 para extrair a chave privada e o certificado X.509. - 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 (
GovClientDurable 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óssleepAfter = '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.pfxem 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 viaopenssl pkcs12 -legacy, aplica a assinatura XMLDSig sobre o XML já gerado em forma canônica e abre a conexãonode:httpscompfxepassphraserumo à 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):
- Antes de chamar a SEFIN: A nota é gravada no D1 com
status: 'unknown'e odpsIdde 45 posições (DPS+ IBGE 7 dígitos +2+ CNPJ 14 dígitos + série 5 dígitos + número 15 dígitos), recuando odhEmiem 60 segundos (CLOCK_SKEW_MS = 60_000) para evitar rejeiçãoE0008por diferença de relógio com o servidor do governo. - Se a conexão cair ou a SEFIN retornar
5xx: A nota permanece emunknown. Na próxima chamada com o mesmoidda venda, o emissor consultaGET /SefinNacional/dps/{dpsId}antes de tentar retransmitir. - 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 erroE0014(duplicidade de DPS). O métodofindIssuedbaixa 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óximodpsNumber(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_SERIESde1a49999): O schema XSD aceita até 5 dígitos (TSSerieDPS), mas a regra de negócioE0010do Anexo I reserva à integração via API exclusivamente a faixa de1a49999(séries50000+pertencem a outros emissores públicos). No PR, propusemos restringirconfigProblemspara acusar erro caso a série seja0, tenha zeros à esquerda ou ultrapasse49999. - Regra
E0713(ISSUER_SIMPLES_OPTIONpara 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 regraE0710exige exatamenteindTotTrib = 0. Porém, para empresas fora do Simples (opSimpNac = '1', Lucro Presumido/Real), a regraE0713proíbe tantoindTotTribquantopTotTribSN, exigindo a abertura de tributos federais, estaduais e municipais (vTotTriboupTotTrib). Enquanto o projeto não implementa esse grupo, o PR propõe queconfigProblemsaceite apenas2(MEI) ou3(ME/EPP), sinalizando o problema no/health(503) e impedindo a chamada de emissão (500emconfigFrom) antes de gerar uma DPS que seria recusada pela SEFIN. - Regra
E0247(taker.emailalinhado aoTSEmail): 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 erroE0247. No PR, propusemos extrairparseIssueRequestparasrc/notas.tscomtrim(), trava de 80 caracteres e validação básica de formato, devolvendo HTTP400imediato.
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-viewerno 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:
- 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
.pfxem variáveis de ambiente nem configurar banco D1. - 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çõesE0039,E0370,E0390eE0713. - 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/emitire 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.
- Quer emitir até 20 notas/mês grátis em produção (ou homologar até 5 CNPJs via Org API) sem subir containers? Crie sua conta gratuita na Notaas.
- Quer integrar em minutos via REST JSON ou MCP Server? Acesse a Documentação Oficial da API Notaas.
- Precisa renderizar o PDF oficial da NT-008 v2.0 no seu projeto? Conheça nossa biblioteca MIT
@notaas/danfse-viewere o playground danfse.notaas.com.br.

