HubProxy is a proxy for GitHub webhooks, built for people building with GitHub at scale. It fixes a lot of stuff, and makes life easier.
It has a lot of features (most optional) and it's extremely configurable.
SQLite/PostgreSQL/MySQL) for audit and replayevents table:original-id-replay-uuidreplayed_fromlimit)type, repository, and senderreplayed_count and list of replayed eventsGET /api/events?forwarded=[true|false]Queue and retry failed deliveries automatically
Security:
Automatically verify GitHub IP origins (often missed in webhooks implementations)
Observability:
Monitor webhook patterns and volume
Development:
HubProxy consists of three main components:
The system is designed to be horizontally scalable and can handle high webhook volumes while maintaining strict delivery guarantees.
SQLite is used by default for development, but PostgreSQL or MySQL are recommended for production:
# SQLite (default for development)
hubproxy --db sqlite:.cache/hubproxy.db
# PostgreSQL
hubproxy --db "postgres://user:password@localhost:5432/hubproxy?sslmode=disable"
# MySQL
hubproxy --db "mysql://user:password@tcp(localhost:3306)/hubproxy"
Here's a simplified version (actual types may vary by database):
CREATE TABLE events (
id VARCHAR(255) PRIMARY KEY, -- GitHub delivery ID
type VARCHAR(50) NOT NULL, -- GitHub event type
payload TEXT NOT NULL, -- Event payload as JSON
headers TEXT, -- HTTP headers as JSON
created_at TIMESTAMP NOT NULL, -- When the event was received
forwarded_at TIMESTAMP, -- When the event was forwarded
error TEXT, -- Error message if delivery failed
repository VARCHAR(255), -- Repository full name
sender VARCHAR(255), -- GitHub username
replayed_from VARCHAR(255), -- Original event ID if this is a replay
original_time TIMESTAMP -- Original event time if this is a replay
);
-- Indexes for efficient querying
CREATE INDEX idx_created_at ON events (created_at);
CREATE INDEX idx_forwarded_at ON events (forwarded_at);
CREATE INDEX idx_type ON events (type);
CREATE INDEX idx_repository ON events (repository);
CREATE INDEX idx_sender ON events (sender);
CREATE INDEX idx_replayed_from ON events (replayed_from);
The storage interface supports filtering events by: - Event type(s) - Repository name - Time range (since/until) - Forwarding status - Sender
Example queries:
// List all events
events, err := storage.ListEvents(QueryOptions{
Types: []string{"push", "pull_request"},
Repository: "owner/repo",
Since: time.Now().Add(-24 * time.Hour),
Forwarded: true,
})
// List only replayed events
events, err := storage.ListEvents(QueryOptions{
Forwarded: false,
})
// List original events that have been replayed
events, err := storage.ListEvents(QueryOptions{
HasReplayedEvents: true,
})
You can query replayed events using the forwarded filter. For example, to list all replayed events:
events, err := storage.ListEvents(QueryOptions{
Forwarded: true,
})
You can also query original events that have been replayed using the HasReplayedEvents filter:
events, err := storage.ListEvents(QueryOptions{
HasReplayedEvents: true,
})
HubProxy allows you to replay webhook events for testing, recovery, or debugging purposes.
Each replayed event has an ID in the format: original-id-replay-uuid
For example:
- Original event ID: d2a1f85a-delivery-id-123
- Replayed event ID: d2a1f85a-delivery-id-123-replay-abc123
This format ensures: 1. Easy tracing back to original event 2. Unique IDs for multiple replays of same event 3. Clear identification of replayed events
// Replay a single event by its ID
event, err := storage.ReplayEvent("d2a1f85a-delivery-id-123")
tools/dev.sh)
Sets up a complete development environment with SQLite database and test server.
```bash
# Start the development environment (required before using other tools)
./tools/dev.sh# Customize webhook secret HUBPROXY_WEBHOOK_SECRET=my-secret ./tools/dev.sh
# Customize test server port
./tools/dev.sh --target-port 8083
``
This will:
- Create a SQLite database in.cache/hubproxy.db`
- Start a test server to receive forwarded webhooks
- Start the webhook proxy with GitHub IP validation disabled
Default settings:
- Webhook secret: dev-secret (via HUBPROXY_WEBHOOK_SECRET env var)
- Test server port: 8082
- SQLite database: .cache/hubproxy.db
internal/cmd/dev/simulate/main.go)
Simulates GitHub webhook events to test the proxy's handling and forwarding.
```bash
# Send test webhooks with the default secret
go run internal/cmd/dev/simulate/main.go --secret dev-secret# Send specific event types go run internal/cmd/dev/simulate/main.go --secret dev-secret --events push,pull_request
# Add delay between events go run internal/cmd/dev/simulate/main.go --secret dev-secret --delay 2s ```
internal/cmd/dev/query/main.go)
Inspects and analyzes webhook events stored in the database.
```bash
# Show recent events
go run internal/cmd/dev/query/main.go# Show event statistics go run internal/cmd/dev/query/main.go --stats
# Filter by event type go run internal/cmd/dev/query/main.go --type push
# Filter by repository go run internal/cmd/dev/query/main.go --repo "owner/repo" ```
internal/cmd/dev/testserver/main.go)
Simple HTTP server that logs received webhooks for verification.
Note: You don't need to run this directly as dev.sh starts it for you.```bash # Start on default port 8082 go run internal/cmd/dev/testserver/main.go
# Start on custom port go run internal/cmd/dev/testserver/main.go --port 8083 ```
To verify events are flowing:
bash
# Watch events in real-time
tail -f .cache/testserver.log
# Run all tests
make test
# Run specific package tests
go test ./internal/storage/...
go test ./internal/webhook/...
# Run with race detection
go test -race ./...
# Test PostgreSQL connection
psql "postgres://user:pass@localhost:5432/hubproxy"
# Test MySQL connection
mysql -h localhost -P 3306 -u user -p hubproxy
# Test SQLite database
sqlite3 .cache/hubproxy.db
HubProxy provides both REST and GraphQL APIs for querying and replaying webhook events.
The API server runs on a separate port (default: 8081) from the webhook handler (default: 8080). This separation allows for different security policies:
Security Recommendations:
Example Nginx Configuration with Basic Auth:
server {
listen 443 ssl;
server_name api.hubproxy.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
auth_basic "HubProxy API";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://localhost:8081;
}
}
All REST API endpoints return JSON responses.
GET /api/events
Lists webhook events with filtering and pagination.
Query Parameters:
- type (optional): Filter by event type (e.g., "push", "pull_request")
- repository (optional): Filter by repository full name (e.g., "owner/repo")
- sender (optional): Filter by GitHub username
- since (optional): Start time in RFC3339 format (e.g., "2024-02-01T00:00:00Z")
- until (optional): End time in RFC3339 format
- forwarded (optional): Filter by forwarding status (true/false)
- limit (optional): Maximum number of events to return (default: 50)
- offset (optional): Number of events to skip for pagination
Response:
{
"events": [
{
"id": "d2a1f85a-delivery-id-123",
"type": "push",
"headers": {
"X-GitHub-Event": ["push"],
"X-GitHub-Delivery": ["d2a1f85a-delivery-id-123"],
"X-Hub-Signature-256": ["sha256=..."]
},
"payload": {
"ref": "refs/heads/main",
"before": "6113728f27ae82c7b1a177c8d03f9e96e0adf246",
"after": "76ae82c7b1a177c8d03f9e96e0adf2466113728f",
"repository": {
"full_name": "owner/repo",
"private": false
},
"pusher": {
"name": "username",
"email": "user@example.com"
}
},
"created_at": "2024-02-06T00:00:00Z",
"forwarded_at": "2024-02-06T00:00:01Z",
"repository": "owner/repo",
"sender": "username"
}
],
"total": 100
}
GET /api/stats
Returns event type statistics for a given time period.
Query Parameters:
- since (optional): Start time in RFC3339 format (default: 24 hours ago)
Response:
{
"push": 50,
"pull_request": 25,
"issues": 10
}
POST /api/events/{id}/replay
Replays a specific webhook event by its ID. The ID should be GitHub's original delivery ID.
Response Fields:
- id: Unique event ID in format original-id-replay-uuid
- type: GitHub event type (e.g., "push", "pull_request")
- payload: Original webhook payload from GitHub
- created_at: When the event was replayed
- forwarded_at: When the event was forwarded (null if not yet forwarded)
- repository: Repository full name
- sender: GitHub username that triggered the event
- replayed_from: ID of the original event that was replayed
Response Example:
{
"id": "d2a1f85a-delivery-id-123-replay-abc123",
"type": "push",
"payload": {
"ref": "refs/heads/main",
"before": "6113728f27ae82c7b1a177c8d03f9e96e0adf246",
"after": "76ae82c7b1a177c8d03f9e96e0adf2466113728f",
"repository": {
"full_name": "owner/repo",
"private": false
},
"pusher": {
"name": "username",
"email": "user@example.com"
}
},
"created_at": "2024-02-06T00:00:00Z",
"forwarded_at": null,
"repository": "owner/repo",
"sender": "username",
"replayed_from": "d2a1f85a-delivery-id-123"
}
```http POST /
$ claude mcp add hubproxy \
-- python -m otcore.mcp_server <graph>