A self-hosted webhook relay service built with Rails 8. Receives webhooks, filters them based on configurable rules, and dispatches to multiple target endpoints.
# Clone and install
git clone https://github.com/mensfeld/hookshot.git
cd hookshot
bundle install
npm install
# Setup database and assets
rails db:create db:migrate
rails tailwindcss:build
# Start everything (web + background jobs)
./bin/dev
# Or start separately:
# rails server # Web server on port 3000
# rails solid_queue:start # Background job processorThen:
- Visit
http://localhost:3000/admin/targets(login:admin/changeme) - Create a target with your destination URL
- Send webhooks to
http://localhost:3000/webhooks/receive
- Webhook Reception: Accepts POST requests at
/webhooks/receiveand stores headers, payload, and metadata - Multiple Targets: Configure multiple destination endpoints for webhook delivery
- Filtering: Route webhooks to specific targets based on header or payload content
- Background Processing: Reliable delivery with Solid Queue, including retries with exponential backoff
- Admin Dashboard: View webhooks, dispatches, and manage targets with a clean DaisyUI interface
- Replay: Re-dispatch any webhook to all active targets
- Health Check:
/healthendpoint for monitoring
- Ruby 3.4+
- SQLite 3
- Node.js (for Tailwind CSS compilation)
# Install dependencies
bundle install
npm install
# Setup database
rails db:create db:migrate
# Compile assets
rails tailwindcss:build
# Start the server
rails server| Variable | Default | Description |
|---|---|---|
TZ |
UTC |
Timezone for displaying timestamps (e.g., Europe/Warsaw, America/New_York) |
HOOKSHOT_USER |
admin |
HTTP Basic Auth username for admin UI |
HOOKSHOT_PASSWORD |
changeme |
HTTP Basic Auth password for admin UI |
RETENTION_DAYS |
30 |
Days to retain webhook data before cleanup |
Start Solid Queue to process webhook deliveries:
rails solid_queue:startOr run everything with Foreman/Overmind using the Procfile.
Send any POST request to /webhooks/receive:
curl -X POST http://localhost:3000/webhooks/receive \
-H "Content-Type: application/json" \
-d '{"event": "user.created", "data": {"id": 123}}'Access the admin UI at http://localhost:3000/admin/webhooks with HTTP Basic Auth.
- Webhooks: View received webhooks, inspect headers/payload, replay to targets
- Dispatches: Monitor delivery status, retry failed deliveries
- Targets: Configure destination endpoints with filters
- Jobs: Solid Queue dashboard at
/jobs
Each target has:
- Name: Identifier for the target
- URL: Destination endpoint (must be HTTPS in production)
- Timeout: Request timeout in seconds (1-300)
- Active: Toggle to enable/disable delivery
- Custom Headers: Additional headers to send with each request
- Filters: Rules to determine which webhooks to deliver
Filters allow routing webhooks to specific targets. All filters must match for delivery.
Filter Types:
Header: Match against request headersPayload: Match against JSON payload using dot notation (e.g.,$.event)
Operators:
Exists: Field is presentEquals: Field equals exact valueMatches: Field matches pattern (supports*wildcard)
Example: Only deliver webhooks where $.event equals user.created:
- Type:
Payload - Field:
$.event - Operator:
Equals - Value:
user.created
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /webhooks/receive |
None | Receive incoming webhooks |
| GET | /health |
None | Health check endpoint |
| GET | /admin/* |
Basic | Admin dashboard |
| GET | /jobs |
Basic | Solid Queue dashboard |
Each delivery includes these headers:
Content-Type: Original webhook content typeX-Hookshot-Webhook-Id: Internal webhook IDX-Hookshot-Delivery-Id: Internal delivery ID- Any custom headers configured on the target
Failed deliveries are retried with exponential backoff:
- Up to 5 attempts
- Increasing delay between retries
- Client errors (4xx) are not retried
- Server errors (5xx) and timeouts are retried
Single container runs both web server and background job processor:
# Build image
docker build -t hookshot .
# Run with docker-compose
SECRET_KEY_BASE=$(rails secret) HOOKSHOT_PASSWORD=your-password docker-compose up -d
# Or run directly
docker run -d \
-p 3000:3000 \
-v hookshot_data:/rails/storage \
-e SECRET_KEY_BASE=$(rails secret) \
-e HOOKSHOT_PASSWORD=your-password \
--name hookshot \
hookshotData is persisted in the hookshot_data volume.
MIT - see LICENSE.md for details.
