O que você leva
Um serviço completo que dá para ler em uma tarde, não um framework. Sua lógica entra em uma única função, handle(). Todo o resto já está pronto e testado.
sender --POST--> /webhook/{source}
1. read the body (size limit)
2. verify the HMAC signature -> 401 if wrong, nothing stored
3. dedupe by delivery id -> the same event twice is handled once
4. insert into SQLite -> 202 Accepted, fast
worker ----------> handle(event) retries with backoff, dead after N attempts
Recursos
- Três esquemas de assinatura. No estilo do GitHub, no estilo do Stripe (com janela contra replay) e o Standard Webhooks, usado pela Polar. A comparação é feita em tempo constante.
- Rotação de segredo.
WEBHOOK_SECRETaceita uma lista separada por vírgulas, então o segredo antigo e o novo valem durante a rotação e nenhum evento se perde. - Deduplicação. Pelo id da entrega (no Standard Webhooks, o header
webhook-idé usado automaticamente) ou, na falta dele, por um hash do corpo. Apoiada em uma constraint de unicidade, ela continua valendo depois de um restart. - Fila durável. SQLite em modo WAL, com a requisição confirmada depois de um único commit. Backoff exponencial, dead-letter depois de
MAX_ATTEMPTStentativas e recuperação dos eventos que um crash deixou emprocessing. - Concorrência.
WORKERS=Nworkers, e um evento nunca é entregue a dois deles. - Operação.
server replaypara os eventos mortos,server purgeeRETENTION_DAYSpara as linhas antigas,/metricsno formato Prometheus e um log de acesso em JSON com request id, que nunca traz payloads, headers nem segredos. - Handlers seguros.
worker.Permanent(err)separa um payload que nunca vai dar certo, sem gastar retries. Se o desligamento pegar o handler no meio, o evento volta para a fila sem consumir uma tentativa. - Exemplos. Um handler para
order.paidda Polar e um roteador JSON genérico, os dois com testes. Dockerfile e unit do systemd incluídos.
Go puro (sem cgo). Dependências: a biblioteca padrão e modernc.org/sqlite.
Medições
Método: o load tester incluso (make loadtest) contra o binário compilado, em um notebook Apple Silicon, com 32 conexões, payloads de 512 bytes, HTTP de verdade via loopback, durabilidade padrão (WAL, synchronous=FULL), 5,000 eventos por execução, três execuções.
| O quê | Resultado |
|---|---|
| Eventos aceitos por HTTP (verificação, insert durável, 202), estilo GitHub | cerca de 7,200 a 9,200 por segundo (p50 2.7 ms, p99 cerca de 17 ms) |
| Idem, Standard Webhooks | cerca de 9,400 por segundo |
| Ponta a ponta, da primeira requisição até a fila vazia (handler que não faz nada), 1 worker | cerca de 3,100 a 3,200 por segundo |
| Fila sendo drenada, handler que dorme 5 ms (como uma chamada de rede), com 1 / 8 / 32 workers | cerca de 163 / 1,463 / 5,264 por segundo |
O custo principal está no banco, não no HTTP: todo acesso ao SQLite passa por uma única conexão. Por isso, mais workers compensam quando o seu handler fica esperando outra coisa, como uma chamada de rede ou outro banco.
Como foi testado
O make test roda go vet e 41 testes com o race detector. Eles cobrem assinaturas adulteradas, vencidas, do futuro e malformadas, o vetor de resposta conhecida do Standard Webhooks tirado da biblioteca Go de referência, rotação de segredo, 401 sem nada gravado, entregas duplicadas, requisições grandes demais ou com método errado, retries e backoff, dead-letter, recuperação após crash, ordenação, vários workers sem nunca processar um evento duas vezes, replay, purge e a atualização de um banco da versão 0.1.0. O binário compilado, gerado a partir do zip do pacote, também foi exercitado com requisições HTTP reais assinadas nos três esquemas, inclusive com o replay da mesma entrega.
Para quem é
Para quem precisa receber webhooks da Polar, do Stripe, do GitHub ou de serviços parecidos e quer as partes pouco glamourosas bem feitas: assinaturas, retries, dedupe e uma fila que sobrevive a um crash.
Foi feito para um servidor só: um único processo, um único escritor no SQLite e eventos tratados em ordem por padrão. A entrega é at-least-once, então o seu handler deve ser idempotente.
Perguntas frequentes
Ele verifica os webhooks da Polar?
Sim. A Polar usa o Standard Webhooks, que é suportado, e o starter inclui um handler de exemplo para os eventos de pedido da Polar.
Posso usar só o código de assinatura?
Pode. Ele está disponível separado, como um módulo pequeno com licença MIT chamado webhooksig. Veja a página de ferramentas grátis.
Preciso de CGO ou de um compilador C?
Não. O driver do SQLite é Go puro.
Qual versão do Go?
Go 1.24 ou mais recente.
Qual é a licença?
Uso nos seus próprios projetos. Não revenda o código-fonte como um starter concorrente.
Changelog
- 0.2.1: driver SQLite atualizado para v1.44.0 (requer Go 1.24 ou superior).
- 0.2.0: Standard Webhooks, rotação de segredo, vários workers, comandos replay e purge,
/metrics, log de acesso, exemplo da Polar, números medidos. - 0.1.0: primeira release.
Ler mais
- Webhook signature checks that fail or quietly pass in Go (em inglês)
- A durable webhook retry queue in SQLite (em inglês)
- Webhook idempotency and dedupe for SQLite and Postgres (em inglês)
- Verify Standard Webhooks (Polar) signatures in Go (em inglês)
- Receiving signed webhooks in Go with a durable queue (em inglês)
Quero este
Pagamento, impostos e notas fiscais ficam com a Polar, que atua como merchant of record. Você recebe o link de download assim que concluir a compra.