Skip to content

Feat/add webhook support pool events#50

Open
mariocodecr wants to merge 10 commits into
SmartDropLabs:mainfrom
mariocodecr:feat/addWebhookSupportPoolEvents
Open

Feat/add webhook support pool events#50
mariocodecr wants to merge 10 commits into
SmartDropLabs:mainfrom
mariocodecr:feat/addWebhookSupportPoolEvents

Conversation

@mariocodecr

@mariocodecr mariocodecr commented Jun 26, 2026

Copy link
Copy Markdown

Pull Request feat: webhook subsystem for pool events (HMAC, retries, rate limiting)

Summary

Add a complete webhook subsystem so projects can register HTTP endpoints that receive notifications when farming/pool
events are indexed. Includes HMAC-SHA256 signing, exponential-backoff retries with a persistent queue, delivery logs for
an admin dashboard, event-type subscriptions, a synthetic test endpoint, and per-IP rate limiting.

Closes #2

Architectural decision: Postgres vs. Redis

The issue spec calls for a webhooks table in PostgreSQL. The project currently runs entirely on Redis (alerts, cache,
jobs). Adding Postgres in this PR would mean introducing the pg driver, a migration tool, Docker/CI updates, and a
schema-bootstrap path — a platform-level change that warrants its own decision and its own PR.

What this PR does instead:

  • Implements a repository pattern (src/repositories/) with the exact PostgreSQL schema the spec asks for
    documented as DDL in each repo's header.
  • Backs the repository with Redis today, mirroring the pattern already used by services/alerts.js.
  • When the team is ready to introduce Postgres, only the two files in src/repositories/ change — the dispatcher,
    routes, worker, tests, and callers remain untouched.

If reviewers prefer to land Postgres now, I'm happy to follow up with a second PR that swaps the implementation; the
contract is already defined.

What's included (mapped to acceptance criteria)

Requirement Where it lives
webhooks table (id, url, events[], secret, active, …) src/repositories/webhookRepository.js (DDL in header)
POST /api/v1/webhooks src/routes/webhooks.js
GET / PATCH / DELETE /api/v1/webhooks/:id src/routes/webhooks.js
Trigger HTTP POST on indexed events src/services/webhookDispatcher.jsdispatch({event_type, event_id, data})
HMAC-SHA256 signature src/services/webhookSignature.js (timing-safe verify)
Retry with exponential backoff, 3 attempts src/services/webhookDispatcher.js + Redis ZSET queue
Persistent retry across restarts src/jobs/webhookRetryWorker.js
Delivery logs (webhook_id, event_id, status, attempts, last_error) src/repositories/deliveryRepository.js (DDL in
header)
Subscription filtering by event type src/services/webhookEvents.js + listActiveForEvent
Rate limiting src/middleware/rateLimit.js (Redis fixed window, fail-open)
Webhook testing endpoint POST /api/v1/webhooks/:id/test
Admin dashboard data GET /api/v1/webhooks/:id/deliveries
Integration docs + examples README.md (Webhooks section)

Retry semantics

  • Retryable: network errors, HTTP 5xx, 408, 429.
  • Not retried: other 4xx (consumer rejected — retrying won't help, marks failed immediately).
  • Backoff: WEBHOOK_RETRY_BASE_MS * WEBHOOK_RETRY_FACTOR ^ (attempts-1) (defaults: 30s → 60s → 120s).
  • Up to WEBHOOK_MAX_ATTEMPTS total attempts (default 3). Survives process restarts via Redis ZSET.

Security

  • Secret is returned in plaintext only on creation and shown with a one-time warning. List/get return secret_preview
    only.
  • HMAC verification uses crypto.timingSafeEqual and length-checks before comparing.
  • /test endpoint is independently rate-limited (5 req/min/IP by default) to prevent abuse as an outbound HTTP cannon.
  • Mgmt rate limiter fails open if Redis is down so a cache outage cannot lock you out.

New env vars (all optional, sane defaults)

Var Default Purpose
WEBHOOK_MAX_ATTEMPTS 3 Total delivery attempts
WEBHOOK_RETRY_BASE_MS 30000 Base backoff
WEBHOOK_RETRY_FACTOR 2 Exponential multiplier
WEBHOOK_TIMEOUT_MS 5000 Per-attempt HTTP timeout
WEBHOOK_RETRY_POLL_MS 5000 Retry worker interval
WEBHOOK_RETRY_BATCH 25 Max retries per tick
WEBHOOK_RATELIMIT_WINDOW / _MAX 60s / 60 Mgmt rate limit
WEBHOOK_TEST_RATELIMIT_WINDOW / _MAX 60s / 5 /test rate limit

Test plan

Automated (npm test): 104 tests / 9 suites passing, including the pre-existing alerts, cors, and resilience
suites (no regressions).

  • npm install && npm test — all 9 suites pass
  • npm run dev boots without errors and logs the retry worker starting
  • curl -X POST http://localhost:3000/api/v1/webhooks -H 'Content-Type: application/json' -d '{"url":"https://webhook.site/<id>","events":["pool.assets_locked"]}' returns 201 with secret and id
  • curl http://localhost:3000/api/v1/webhooks lists the webhook with secret_preview (not secret)
  • curl -X POST http://localhost:3000/api/v1/webhooks/<id>/test produces a delivery on webhook.site with
    X-SmartDrop-Signature: sha256=…
  • Verify the signature locally with the Node example in the README
  • Update the webhook URL to a non-existent host and trigger /test — confirm delivery row goes to pending with
    next_retry_at set, and that the retry worker re-processes it
  • curl http://localhost:3000/api/v1/webhooks/<id>/deliveries shows the delivery history
  • Hammer POST /api/v1/webhooks/<id>/test more than 5x/min — confirm 429 with X-RateLimit-* headers
  • PATCH the webhook to {"active": false} and confirm dispatch no longer routes to it

Introduce the foundational primitives the webhook subsystem will build on:
the catalogue of pool/price event types (with wildcard support) and a
timing-safe HMAC-SHA256 sign/verify helper plus secret generator.
Model the future PostgreSQL schema for webhooks and webhook_deliveries
behind a repository abstraction backed by Redis today. The SQL DDL is
documented inline so migrating to Postgres only requires swapping the
repository implementation. Includes a reusable in-memory cache mock so
the existing Jest pattern keeps working across the new test files.
The dispatcher fans an event out to every active webhook subscribed to
its type, persists a delivery record per webhook, signs the body with
HMAC-SHA256, and retries failed deliveries up to WEBHOOK_MAX_ATTEMPTS.

Retry decisions follow standard webhook semantics: network errors and
5xx/408/429 responses are retried with exponential backoff, while other
4xx responses are marked failed immediately so a misconfigured consumer
cannot be retried into the ground. Retries are queued in a Redis ZSET
so they survive process restarts.

All webhook subsystem config knobs are introduced here so callers in
later commits can rely on them.
Poll the Redis ZSET of due delivery retries on a configurable interval
and re-invoke the dispatcher for each one. A simple running flag avoids
overlapping ticks if a previous batch is still in flight.
Generic per-IP limiter built on INCR + EXPIRE so it can be reused by any
route by tagging it with a keyPrefix. The middleware fails open if Redis
is unreachable so a cache outage cannot lock users out of management
endpoints, and surfaces standard X-RateLimit-* headers on every response.
Add CRUD endpoints for webhook registration, a dedicated test-delivery
endpoint (rate-limited tighter than mgmt to prevent abuse as an outbound
HTTP cannon) and a deliveries listing endpoint that powers the admin
dashboard view of webhook health.

The secret is returned in plaintext only on creation and never echoed
back from list/get; subsequent reads only expose a short preview.
Wire the webhook subsystem into the Express bootstrap and shut the retry
worker down cleanly on SIGTERM/SIGINT alongside the existing price job.
…semantics

Add a full Webhooks section to the README: supported event types, the
public REST API surface, the outgoing request shape (headers + body),
a Node.js HMAC verification example, retry/backoff behavior, the
repository-pattern note explaining the PG migration path, rate-limit
defaults and the new env vars.
@prodbycorne

Copy link
Copy Markdown
Contributor

resolve merge conflicts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Webhook Support for Pool Events (Discord, Slack, Custom)

2 participants