Pular para o conteúdo
Nelcota
EN : Read in English

Eventos e webhooks

6 min de leitura Editar no GitHub

Envie alterações confirmadas do banco para um endpoint HTTP. Configuração, chaves de assinatura e histórico de entregas ficam em Eventos no painel. O mesmo binário Rust envia as entregas; o Postgres armazena a fila, então reiniciar não perde trabalhos pendentes.

Pré-requisitos

Use servidor 0.4.0+ com acesso administrativo e receptor confiável. O receptor precisa de verificação de assinatura no corpo bruto com tamanho limitado e deduplicação durável por ID de evento. Mudanças não publicadas de agendamento/ciclo de vida ficam em Unreleased.

Criar um webhook

  1. Abra Eventos → Novo webhook.
  2. Dê um nome, escolha uma tabela e selecione INSERT, UPDATE e/ou DELETE.
  3. Informe a URL HTTPS pública que recebe requisições POST com JSON.
  4. Mantenha as seis tentativas padrão ou escolha de uma a dez.
  5. Salve e copie a chave de assinatura para o serviço que receberá os eventos. Ela aparece uma única vez.

Esta versão aceita apenas tabelas comuns do schema exposto. Views, tabelas particionadas, metadados de arquivos e tabelas de autenticação ficam de fora. Renomear uma tabela preserva seus webhooks, pois a captura acompanha a identidade do objeto no PostgreSQL. Excluir e recriar a tabela exige um novo webhook.

Pausar interrompe a captura de novas alterações e retém as entregas pendentes. Reativar retoma a fila; alterações feitas durante a pausa não são recuperadas. Excluir cancela trabalhos pendentes e remove a chave, preservando o histórico concluído. Um envio já iniciado ainda pode chegar depois de uma pausa, exclusão ou troca de chave.

Editar a URL afeta os novos eventos. Entregas na fila mantêm o destino original. Reenviar usa a URL, a chave e o limite de tentativas atuais do webhook. A troca de chave vale para envios iniciados depois dela; atualize o serviço receptor antes de reativar um webhook pausado.

Dados do evento

O serviço receptor recebe um objeto JSON. O ID do evento permanece igual em novas tentativas, reenvios e destinos que recebem a mesma alteração do banco. Cada destino e reenvio manual tem seu próprio ID de entrega.

JSON
{
  "version": 1,
  "id": "94ca9597-b0d8-4c4c-a8ba-e5052c031382",
  "schema": "public",
  "table": "orders",
  "type": "UPDATE",
  "occurred_at": "2026-10-11T12:00:00Z",
  "record": { "id": 42, "status": "paid" },
  "old_record": { "id": 42, "status": "pending" }
}

Em INSERT, old_record é nulo. Em DELETE, record é nulo. Alterações por REST, RPC, painel ou SQL direto são capturadas pelo mesmo trigger. O evento é confirmado junto com a linha alterada; uma transação revertida não cria entrega. A captura nunca faz chamadas de rede.

O destino recebe a linha alterada completa e, em atualizações e exclusões, a linha anterior. Apenas administradores do projeto configuram destinos. Os papéis da API pública não podem consultar a fila ou as chaves; RLS continua decidindo quais linhas esses papéis podem alterar. Webhooks são integrações privilegiadas entre servidores, e não assinaturas filtradas por usuário.

Verificar assinaturas

Cada requisição inclui estes cabeçalhos:

Cabeçalho Significado
x-nelcota-event-id UUID estável do evento; use para evitar processamento duplicado de forma persistente
x-nelcota-delivery-id Identificador da entrega mostrado no painel
x-nelcota-timestamp Horário Unix em segundos deste envio
x-nelcota-signature v1= seguido pelo resumo HMAC-SHA256 hexadecimal em letras minúsculas

Os bytes assinados são o ID do evento, um ponto, o horário, outro ponto e o corpo exato da requisição. A chave HMAC é a string literal da chave de assinatura, incluindo seu prefixo. Use o corpo bruto antes de interpretar o JSON, recuse horários distantes mais de cinco minutos do relógio do servidor, compare em tempo constante e confira se o ID do conteúdo corresponde ao cabeçalho assinado.

JavaScript
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyWebhook(headers, rawBody, secret) {
  const id = headers.get('x-nelcota-event-id') ?? '';
  const timestamp = headers.get('x-nelcota-timestamp') ?? '';
  const signature = headers.get('x-nelcota-signature') ?? '';
  if (!/^[a-f0-9-]{36}$/.test(id) || !/^\d{1,12}$/.test(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  if (!/^v1=[a-f0-9]{64}$/.test(signature)) return false;
  const expected = createHmac('sha256', secret)
    .update(`${id}.${timestamp}.`).update(rawBody).digest();
  return timingSafeEqual(expected, Buffer.from(signature.slice(3), 'hex'));
}

Limite o corpo recebido a 256 KiB. Depois de verificar, interprete o JSON e processe uma vez por ID de evento em uma transação no banco com uma restrição de unicidade. Registre a aceitação de forma persistente antes de responder com 2xx. Um evento repetido também deve receber 2xx, sem repetir a ação. Para vários consumidores independentes que compartilham um banco, evite duplicação por consumidor e ID do evento.

Novas tentativas e histórico de entregas

Qualquer resposta 2xx confirma a entrega. Erros de rede, timeouts, HTTP 408, 425, 429 e 5xx recebem novas tentativas automaticamente. Os demais estados, incluindo redirecionamentos, encerram a entrega com falha. O receptor tem dez segundos para responder; um prazo separado de doze segundos também limita DNS e conexão.

Os intervalos começam em aproximadamente 5 segundos, 30 segundos, 2 minutos, 10 minutos, 30 minutos e 1 hora, com variação e limite de uma hora. Um cabeçalho numérico Retry-After é respeitado até uma hora, sem reduzir o intervalo normal. As seis tentativas padrão incluem o primeiro envio e cinco repetições. Um trabalho perdido pelo worker é recuperado após dois minutos e ainda conta como tentativa.

A entrega ocorre pelo menos uma vez, sem garantia de ordem. Um receptor pode processar um evento cuja confirmação nunca chega ao Nelcota; a próxima tentativa pode enviá-lo novamente. Vários workers assumem linhas da fila por períodos curtos e não retêm uma conexão com o banco durante chamadas HTTP.

O histórico mostra estado, código HTTP, tentativas, horários e o conteúdo exato. Os detalhes da entrega mostram cada tentativa, incluindo envios interrompidos por um reinício. Reenviar cria uma nova entrega, preservando o histórico original e o ID estável do evento. Entregas ativas não podem ser reenviadas; o webhook precisa estar ativo. Corpos das respostas do receptor e chaves de assinatura nunca aparecem no histórico.

Limites e retenção

Cada projeto aceita até 50 configurações ativas e dez por tabela. A fila aceita até 10.000 entregas pendentes e 100.000 entregas retidas. A admissão é transacional: ao atingir um limite, a gravação falha com um erro de capacidade que permite nova tentativa, em vez de perder silenciosamente seu evento. O painel mostra o uso e avisa aos 80%.

O histórico concluído é mantido por até sete dias e limpo em lotes limitados. Entregas pendentes nunca são excluídas por idade. Limpar histórico remove apenas entregas concluídas e seus conteúdos. Para liberar uma fila parada, corrija ou ative seus destinos, ou exclua webhooks indesejados para cancelar seus trabalhos pendentes. Mantenha os backups do banco privados: eles contêm chaves de assinatura e conteúdos de eventos.

Um conteúdo maior que 256 KiB produz uma entrega visivelmente falha com dados omitidos, enquanto a linha de negócio ainda é confirmada. Essa entrega não pode ser reenviada; reduza o tamanho da linha ou use outra integração para valores grandes. Alterações na configuração de um webhook são configurações do projeto, separadas da exportação de migrações do schema.

Segurança dos destinos e desenvolvimento local

Destinos públicos exigem HTTPS com verificação de certificado. Credenciais, parâmetros e fragmentos nas URLs são recusados. Redirecionamentos e proxies do ambiente ficam desativados. IPs privados, de loopback, link-local, metadados, NAT compartilhado, multicast e faixas reservadas são bloqueados, incluindo respostas DNS no momento da conexão. Todos os endereços de uma resposta DNS precisam passar pela política, impedindo mudanças de DNS que direcionem chamadas a serviços internos.

Apenas no desenvolvimento local, defina NELCOTA_WEBHOOKS_ALLOW_LOOPBACK=true e reinicie. Isso permite HTTP ou HTTPS para localhost, 127.0.0.1 e ::1; nunca libera redes privadas ou endereços de metadados. Dentro do Docker, loopback aponta para o contêiner do Nelcota, não para o host. Use um túnel HTTPS público de desenvolvimento quando o receptor estiver fora desse contêiner.

O envio não exige Redis, extensão HTTP, outro servidor ou processo Node. O worker da fila usa um pool pequeno separado das conexões da API e do painel. O encerramento normal para de assumir trabalhos e dá tempo para terminar as entregas iniciadas; trabalhos incompletos continuam recuperáveis na próxima inicialização.

Veja API REST, painel, backup e arquitetura.