Skip to content

Mukul-svg/notchd

Repository files navigation

notchd

Zero-dependency webhook ingestion runtime.

notchd sits between webhook providers (Stripe, GitHub, Slack) and your app. It receives webhooks instantly, persists them to an embedded SQLite database, and delivers them to your app with automatic retry, dead-letter queue, and event replay — all in a single binary with no external dependencies.

No Redis. No PostgreSQL. No message queue. One binary, one config file.

go install github.com/mukul-svg/notchd/cmd/notchd@latest

The Problem

When Stripe sends a webhook, it expects a response in under 3 seconds. If your handler is slow, Stripe retries and you process the same payment twice. If your app is down, the event is lost after Stripe's retry window closes.

The correct solution — ACK immediately, process in the background — requires a persistent queue, retry logic, a dead-letter queue, and signature verification. Every team rebuilds this from scratch.

notchd is that infrastructure, ready to run.

Without notchd:
  Stripe → Your App → [process synchronously] → 200 OK
                           ↑ slow = duplicate. crash = lost.

With notchd:
  Stripe → notchd → [saved in < 5ms] → 200 OK
                         ↓
                   [background] → Your App → 200 OK
                                      ↑ slow = fine. crash = retried.

Quick Start

1. Install

go install github.com/mukul-svg/notchd/cmd/notchd@latest

2. Create notchd.yaml

delivery:
  target_url: "http://localhost:3000/webhooks"

management:
  api_key: "${NOTCHD_API_KEY}"

endpoints:
  - name: stripe
    path: "/webhooks/stripe"
    signature:
      provider: stripe
      secret: "${NOTCHD_STRIPE_SECRET}"

3. Set secrets and run

export NOTCHD_API_KEY="your-management-key"
export NOTCHD_STRIPE_SECRET="whsec_..."

notchd start

4. Point Stripe to http://your-server:8080/webhooks/stripe

That's it. notchd handles everything else.


What notchd Does

  • Receives webhooks and ACKs in under 5ms
  • Verifies signatures from Stripe, GitHub, Slack (provider-specific algorithms, not generic HMAC)
  • Persists every event to SQLite before returning 200
  • Delivers events to your app asynchronously (5 concurrent workers by default)
  • Retries failed deliveries with exponential backoff (30s → 2m → 10m → 30m → 2h)
  • 4xx responses → event moves to DLQ immediately (no pointless retries)
  • Dead-letter queue → inspect and replay failed events via CLI or API
  • Interactive TUI → real-time visual dashboard (notchd dashboard) to monitor events
  • Survives crashes → startup recovery re-enqueues orphaned events
  • Graceful shutdown → SIGTERM drains in-flight deliveries before exit

Signature Verification

notchd implements each provider's exact algorithm — not a generic HMAC wrapper.

Provider Header Algorithm
Stripe Stripe-Signature HMAC-SHA256 of {timestamp}.{body}, timestamp tolerance, multi-signature rotation support
GitHub X-Hub-Signature-256 HMAC-SHA256 of body, sha256= prefix
Slack X-Slack-Signature + X-Slack-Request-Timestamp HMAC-SHA256 of v0:{timestamp}:{body}, timestamp tolerance
Generic HMAC Configurable HMAC-SHA256, configurable header and prefix
None No verification (useful for internal webhooks)

All comparisons use hmac.Equal (constant-time) to prevent timing attacks.


CLI

notchd start                          # Start the server
notchd dashboard                      # Open the interactive terminal UI
notchd status                         # Show runtime statistics
notchd events list                    # List recent events
notchd events list --status=dlq       # List DLQ events
notchd events inspect <id>            # Full event detail + delivery history
notchd events replay <id>             # Re-deliver an event
notchd dlq list                       # List dead-letter queue
notchd version                        # Print version

Global flags:

--config    Path to config file (default: notchd.yaml)
--db        Path to database file (overrides config)
--api-url   Management API URL (default: http://127.0.0.1:9001)
--api-key   Management API key (overrides NOTCHD_API_KEY)

Management API

Runs on 127.0.0.1:9001 by default. All endpoints require Authorization: Bearer <key> except /health.

GET  /health                   No auth — for load balancer checks
GET  /stats                    Runtime statistics
GET  /events                   List events (filter by status, endpoint, date)
GET  /events/{id}              Full event detail + delivery attempts
POST /events/{id}/replay       Re-enqueue an event for delivery
GET  /dlq                      List DLQ events
POST /dlq/replay-all           Bulk replay all DLQ events
DELETE /dlq/{id}               Permanently delete a DLQ event

Configuration

server:
  port: 8080
  host: "0.0.0.0"
  max_body_size_kb: 1024

management:
  port: 9001
  host: "127.0.0.1"          # Bind to localhost — never expose publicly
  api_key: "${NOTCHD_API_KEY}"

database:
  path: "./notchd.db"

delivery:
  target_url: "http://localhost:3000/webhooks"
  timeout_seconds: 30
  workers: 5

retry:
  max_attempts: 6             # 1 initial + 5 retries → DLQ
  initial_delay_seconds: 30
  multiplier: 4.0             # 30s → 2m → 8m → 32m → 2h
  jitter_percent: 10
  max_delay_hours: 24

shutdown:
  timeout_seconds: 30

dlq:
  retention_days: 30

log:
  level: "info"               # debug | info | warn | error
  format: "json"              # json | text

endpoints:
  - name: stripe
    path: "/webhooks/stripe"
    signature:
      provider: stripe
      secret: "${NOTCHD_STRIPE_SECRET}"

  - name: github
    path: "/webhooks/github"
    signature:
      provider: github
      secret: "${NOTCHD_GITHUB_SECRET}"

  - name: slack
    path: "/webhooks/slack"
    signature:
      provider: slack
      secret: "${NOTCHD_SLACK_SECRET}"

All config values can be overridden via NOTCHD_ env vars (e.g. NOTCHD_DELIVERY_WORKERS=10).


Delivery Headers

notchd forwards the original payload to your app and adds these headers:

Header Value
Notchd-Event-ID Unique ULID for this event (stable across retries and replays)
Notchd-Attempt Total attempt number across all retries and replays (1-indexed)
Notchd-Timestamp When notchd originally received the event (RFC 3339)
Notchd-Is-Replay true if this delivery was triggered by a manual replay

Use Notchd-Event-ID to deduplicate events in your app — notchd provides at-least-once delivery semantics.


Compared to Alternatives

notchd Convoy inhooks Hookdeck
External dependencies None Postgres + Redis + MQ Redis SaaS
Single binary Yes No No No
Provider-specific signatures Yes Partial Generic HMAC only Yes
DLQ + replay Yes Yes No Yes
Self-hosted Yes Yes Yes No (SaaS)
Free Yes Yes Yes Paid tiers

vs. adnanh/webhook — that tool runs shell scripts triggered by webhooks. It has no persistence, no retry, no delivery guarantees. Different use case entirely.


How It Works

notchd uses SQLite (WAL mode) as both the event store and the queue backend (goqite). No external processes required.

Provider → notchd ingestion server → SQLite (event + queue) → Worker pool → Your App
                    ↑ < 5ms                                        ↑ async, retried

On crash: SQLite WAL rolls back uncommitted writes. Startup recovery re-enqueues orphaned events.


License

Apache 2.0

Packages

 
 
 

Contributors

Languages