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
- Producers submit events via
POST /eventswith anX-Api-Keyheader. - 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. - 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. - 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. - 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. - 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
| TABLE | PURPOSE |
|---|---|
| tenants | Multi-tenant account credentials, API hashes, and rate limit definitions. |
| endpoints | Registered webhook target URLs with signing secrets, filters, and status flags. |
| events | Payload backups, event types, and idempotency key references. |
| deliveries | Delivery records mapping events to endpoints and scheduling retry timings. |
| delivery_attempts | Audit 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
| METRIC | VALUE |
|---|---|
| Throughput | 1,259 req/s |
| p50 Latency | 73 ms |
| p95 Latency | 166 ms |
| Error Rate (>500ms) | 0.12% |
| HTTP failure rate | 0.00% |
Optimization History
| CHANGE STAGE | p50 | p95 | THROUGHPUT |
|---|---|---|---|
| Baseline Node API | 247 ms | 709 ms | 342 req/s |
| Fastify Schemas & Pino Logging | 190 ms | 444 ms | 455 req/s |
| Redis Buffer + Batch SQL Inserts | 73 ms | 166 ms | 1,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.
| VARIABLE | DESCRIPTION | DEFAULT |
|---|---|---|
| DATABASE_URL | PostgreSQL connection string. | Required |
| REDIS_URL | Redis connection string. | Required |
| ADMIN_SECRET | Secret for admin API endpoints (min 32 chars). | Required |
| PORT | API server port. | 8080 |
| NODE_ENV | development, production, or test. | development |
| MAX_DELIVERY_ATTEMPTS | Max delivery attempts before a delivery is moved to dead status. | 10 |
| DELIVERY_TIMEOUT_MS | HTTP request timeout per delivery attempt. | 30000 |
| CIRCUIT_BREAKER_THRESHOLD | Consecutive failures before circuit opens. | 5 |
| CIRCUIT_BREAKER_COOLDOWN_MS | Time before circuit transitions to HALF-OPEN for a probe request. | 60000 |
| RATE_LIMIT_WINDOW_MS | Fixed-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.
Delivery Pipeline
Webhooks pass through a fast and reliable pipeline to isolate failures and guarantee payload delivery.
- Redis Buffering: The API responds in
202 Acceptedafter a singleLPUSH— 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 bulkINSERTandaddBulkto 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.
| ATTEMPT | BACKOFF DELAY |
|---|---|
| Attempt 1 | 10 seconds |
| Attempt 2 | 30 seconds |
| Attempt 3 | 1 minute |
| Attempt 4 | 5 minutes |
| Attempt 5 | 30 minutes |
| Attempt 6 | 1 hour |
| Attempt 7 | 3 hours |
| Attempt 8 | 6 hours |
| Attempt 9 | 12 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
| COMPONENT | TECHNOLOGY |
|---|---|
| Language runtime | TypeScript (Node.js >= 18) |
| API gateway framework | Fastify 5 (High-speed schema validations) |
| Background queues | BullMQ (Bull Redis queues) |
| Relational database | PostgreSQL 16 |
| Database ORM mapping | Drizzle ORM |
| Caching & rate metrics | Redis 7 (Lua atomic scripts) |