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@latestWhen 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.
1. Install
go install github.com/mukul-svg/notchd/cmd/notchd@latest2. 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 start4. Point Stripe to http://your-server:8080/webhooks/stripe
That's it. notchd handles everything else.
- 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
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.
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 versionGlobal 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)
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
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).
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.
| 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.
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.
Apache 2.0