Documentation

HookRelay Documentation

Welcome to the complete developer reference guide for HookRelay. Learn how to configure, deploy, scale, and integrate our reliable high-throughput webhook delivery infrastructure.

Architecture

HookRelay accepts events from producers via a REST API, buffers them in Redis, and guarantees delivery to endpoints through BullMQ workers. The architecture supports horizontal scaling and automatic recovery.

Data Flow

  1. Producers submit events via POST /events with an X-Api-Key header.
  2. The API authenticates (Redis-cached tenant lookup, 60s TTL), applies the Lua fixed-window rate limiter, checks idempotency via Redis GET, then pushes the event to the Redis buffer (LPUSH) and immediately returns 202.
  3. The Ingest Worker runs a poll loop with BRPOP (blocking pop). When data arrives it uses an atomic Lua script (LRANGE + LTRIM) to pop up to 100 events in a single round-trip, batch-inserts them into PostgreSQL, bulk-enqueues fanout jobs via BullMQ, and sets Redis idempotency keys for successful inserts.
  4. The Fanout Worker (concurrency=10) reads fanout jobs from BullMQ, queries PostgreSQL for matching active endpoints (respecting eventTypeFilter), batch-inserts delivery rows, bulk-enqueues delivery jobs into the per-tenant delivery queue, and publishes a notification to Redis Pub/Sub so the Delivery Worker Manager can spin up a new worker for new tenants.
  5. Delivery Workers (concurrency=50 per tenant) process jobs from per-tenant BullMQ queues. They fetch endpoint config and event payloads from Redis cache (falling back to Postgres), check the circuit breaker state, sign the payload with HMAC-SHA256, and POST to the endpoint URL via undici.
  6. On success: delivery attempt is recorded and circuit breaker is reset. On failure: attempt is recorded, circuit breaker failure count is incremented, and a retry is scheduled with exponential backoff (10s → 30s → 1m → 5m → ... → 24h). After max attempts the delivery is marked dead.

Database Schema

TABLEPURPOSE
tenantsMulti-tenant account credentials, API hashes, and rate limit definitions.
endpointsRegistered webhook target URLs with signing secrets, filters, and status flags.
eventsPayload backups, event types, and idempotency key references.
deliveriesDelivery records mapping events to endpoints and scheduling retry timings.
delivery_attemptsAudit log tracking HTTP response statuses, raw error messages, and latency profiles.

Performance Benchmarks

HookRelay is benchmarked using k6 on a local Docker framework. Ingestion throughput processes over **1,259 requests per second** concurrently.

Throughput Profile

METRICVALUE
Throughput1,259 req/s
p50 Latency73 ms
p95 Latency166 ms
Error Rate (>500ms)0.12%
HTTP failure rate0.00%

Optimization History

CHANGE STAGEp50p95THROUGHPUT
Baseline Node API247 ms709 ms342 req/s
Fastify Schemas & Pino Logging190 ms444 ms455 req/s
Redis Buffer + Batch SQL Inserts73 ms166 ms1,259 req/s

Project Structure

HookRelay is organized as a pnpm workspace monorepo divided into applications (`apps`) and packages (`packages`).

hookrelay/
├── apps/
│   ├── api/                    # Fastify HTTP server ingestion gateway
│   │   └── src/
│   │       ├── main.ts         # Server entry point
│   │       ├── routes/         # REST API routes (events, tenants, endpoints)
│   │       └── middleware/     # Rate limiter & auth filters
│   ├── worker/                 # Background job worker orchestrators
│   │   └── src/
│   │       └── workers/        # BullMQ worker processors (ingest, fanout, delivery, dlq)
│   ├── mock-server/            # Webhook consumer mock server
│   └── web/                    # Next.js dashboard for tenants and endpoints
|
├── packages/
|   ├──config/                    # Environment configuration
│   ├── db/                     # Drizzle ORM schema, push migrations, queries
│   ├── lib/                    # Redis client, logger (pino), HTTP client (undici)
│   ├── queue/                  # BullMQ definitions, Redis connection clients
│   ├── services/               # Ingest, fanout, circuit-breakers, signature validators
│   └── typescript-config/      # Workspace shared typescript settings
├── k6/                         # Load test scripts
├── docker/
│   ├── Dockerfile.api
│   └── Dockerfile.worker
└── docker-compose.yml          # Container configuration

Prerequisites

  • Node.js: version 18 or higher.
  • pnpm: version 9.0 or higher.
  • Docker / Docker Compose: for running database containers.
  • PostgreSQL 16 and Redis 7.

Getting Started

Running with Docker (Recommended)

# 1. Clone repository
git clone https://github.com/SoloDevAbu/HookRelay.git
cd HookRelay

# 2. Copy env configs
cp .env.example .env

# 3. Spin up environment containers
docker compose up -d

Running without Docker

# Install workspace dependencies
pnpm install

# Run database push migrations
pnpm --filter @hookrelay/db db:push

# Launch API and Worker tasks
pnpm --filter @hookrelay/api dev
pnpm --filter @hookrelay/worker dev

Configuration Settings

Settings are validated at startup using Zod schemas. Configure these using `.env` file variables.

VARIABLEDESCRIPTIONDEFAULT
DATABASE_URLPostgreSQL connection string.Required
REDIS_URLRedis connection string.Required
ADMIN_SECRETSecret for admin API endpoints (min 32 chars).Required
PORTAPI server port.8080
NODE_ENVdevelopment, production, or test.development
MAX_DELIVERY_ATTEMPTSMax delivery attempts before a delivery is moved to dead status.10
DELIVERY_TIMEOUT_MSHTTP request timeout per delivery attempt.30000
CIRCUIT_BREAKER_THRESHOLDConsecutive failures before circuit opens.5
CIRCUIT_BREAKER_COOLDOWN_MSTime before circuit transitions to HALF-OPEN for a probe request.60000
RATE_LIMIT_WINDOW_MSFixed-window duration for per-tenant rate limiting.60000

API Reference

All client endpoints require client authorization header key: X-Api-Key. Admin tasks require: X-Admin-Key.

POST/tenants
Admin Access Only

Registers a new client tenant workspace. Returns the raw API credential key.

CURL COMMAND
curl -X POST http://localhost:3000/tenants \
  -H "X-Admin-Key: <secret>" \
  -d '{"name": "production-app"}'
JSON RESPONSE (201)
{
  "success": true,
  "data": {
    "tenant": { "id": "uuid", "name": "production-app" },
    "apiKey": "hr_prod_key_..."
  }
}
POST/events
X-Api-Key Required

Ingests a new webhook payload. Loops to buffer queue and returns 202 accepted.

CURL COMMAND
curl -X POST http://localhost:3000/events \
  -H "X-Api-Key: wh_live_..." \
  -d '{
    "eventType": "order.placed",
    "payload": {"id": 12},
    "idempotencyKey": "order-12"
  }'
JSON RESPONSE (202)
{
  "success": true,
  "data": {
    "eventId": "uuid-event",
    "duplicate": false,
    "status": "accepted"
  }
}

Delivery Pipeline

Webhooks pass through a fast and reliable pipeline to isolate failures and guarantee payload delivery.

Producer → API (auth + rate limit + idempotency) → Redis Buffer → Ingest Worker (batch SQL + fanout jobs) → Fanout Worker (endpoints + delivery rows + Pub/Sub) → Per-Tenant Delivery Queue → Delivery Worker (circuit breaker + HMAC + HTTP) → Consumer Endpoint
  • Redis Buffering: The API responds in 202 Accepted after a single LPUSH — no SQL on the hot path. Your producers never wait on DB latency.
  • Atomic Lua Batch Pop: The Ingest Worker uses a custom Lua script (LRANGE + LTRIM) to atomically pop up to 100 events in a single Redis round-trip, then performs a single bulk INSERT and addBulk to BullMQ.
  • Fanout & Pub/Sub: The Fanout Worker resolves matching endpoints from Postgres and enqueues delivery jobs into a per-tenant BullMQ queue. It then publishes a Redis Pub/Sub message so the Delivery Worker Manager can spin up a new worker process for new tenants on-the-fly.
  • Tenant Isolation: Every tenant gets its own BullMQ delivery queue and dedicated worker (concurrency=50). One slow or unreachable destination never delays deliveries for other tenants.
  • Caching in Delivery: Delivery Workers cache endpoint config (5 min TTL) and event payloads (1 hr TTL) in Redis to eliminate N+1 Postgres queries when the same event fans out to many endpoints.

Retry Strategy

Failed delivery calls (non-2xx response, timeout, or network error) automatically retry using a fixed backoff schedule defined in retry.service.ts. After MAX_DELIVERY_ATTEMPTS (default 10) the delivery is marked dead.

ATTEMPTBACKOFF DELAY
Attempt 110 seconds
Attempt 230 seconds
Attempt 31 minute
Attempt 45 minutes
Attempt 530 minutes
Attempt 61 hour
Attempt 73 hours
Attempt 86 hours
Attempt 912 hours
Attempt 10 (Final)24 hours → marked dead

Circuit Breaker

Each destination endpoint operates under an isolated Redis-backed circuit breaker state machine:

  • CLOSED: Normal operation. Delivery jobs execute immediately.
  • OPEN: Triggered after CIRCUIT_BREAKER_THRESHOLD (default 5) consecutive failures. Incoming delivery jobs for this endpoint are requeued into the per-tenant BullMQ queue with a 30-second delay (no attempt is counted), preventing wasted HTTP calls against a clearly-down endpoint.
  • HALF-OPEN: After CIRCUIT_BREAKER_COOLDOWN_MS (default 60s), the state transitions to HALF-OPEN. One probe delivery is allowed through. If it returns 2xx the circuit closes and the failure counter resets. If it fails the circuit re-opens.

Rate Limiting

Rate limiting is active per-tenant using Redis counters. If a workspace exceeds its rate limits (e.g. 1,000 req/min), HookRelay returns a 429 response block containing a standard Retry-After header specifying when ingestion re-opens.

Load Testing

k6 test configurations exist inside the project repository root for auditing local deployments:

# Run full ingestion load test (200 VUs, 5 minutes)
k6 run k6/load-test.js

# Validate rate limiting response headers
k6 run k6/rate-limit-test.js

Tech Stack

COMPONENTTECHNOLOGY
Language runtimeTypeScript (Node.js >= 18)
API gateway frameworkFastify 5 (High-speed schema validations)
Background queuesBullMQ (Bull Redis queues)
Relational databasePostgreSQL 16
Database ORM mappingDrizzle ORM
Caching & rate metricsRedis 7 (Lua atomic scripts)