Introduction
Composable webhook delivery engine for Go.
Relay is a Go library that handles webhook delivery for multi-tenant SaaS applications. Import it into your Go program to get dynamic event type definitions, tenant-scoped endpoints, guaranteed delivery with HMAC-SHA256 signatures, retry with exponential backoff, dead letter queues, and replay capabilities.
Relay is a library -- not a service. You bring your own HTTP server, database, and auth layer. Relay provides the webhook plumbing.
What it does
- Dynamic webhook catalog -- Register event types at runtime with optional JSON Schema validation.
- Tenant-scoped endpoints -- Each endpoint belongs to a tenant and subscribes to event types via glob patterns.
- Guaranteed delivery -- Exponential backoff retries (default: 5s, 30s, 2m, 15m, 2h). Failed deliveries land in the dead letter queue.
- HMAC-SHA256 signatures -- Every delivery is signed. Receivers verify authenticity with a single function call.
- Rate limiting -- Per-endpoint token bucket limiter prevents overloading downstream services.
- Admin HTTP API -- Full CRUD for event types, endpoints, events, deliveries, and DLQ replay.
- Observability -- Prometheus counters, latency histograms, DLQ gauges, and OpenTelemetry tracing per delivery.
- Pluggable storage -- Memory, PostgreSQL (pgx/v5), or Bun ORM. Implement
store.Storefor anything else.
Design philosophy
Library, not framework. Relay is a set of Go packages you import, not a binary you deploy. You control the main function, the HTTP server, and the process lifecycle.
Interfaces over implementations. Every subsystem defines a Go interface. The library ships with defaults (in-memory store, built-in sender), but you can replace any piece.
Composable stores. Each subsystem has its own store interface. The aggregate store.Store composes them all. Swap the entire backend with a single type.
Multi-tenant by default. Every endpoint and event is scoped to a tenant ID. The scope package captures context-level app and organization IDs.
Quick look
package main
import (
"context"
"encoding/json"
"log"
"github.com/xraph/relay"
"github.com/xraph/relay/catalog"
"github.com/xraph/relay/endpoint"
"github.com/xraph/relay/event"
"github.com/xraph/relay/store/memory"
)
func main() {
ctx := context.Background()
r, err := relay.New(relay.WithStore(memory.New()))
if err != nil {
log.Fatal(err)
}
r.RegisterEventType(ctx, catalog.WebhookDefinition{
Name: "order.created",
Description: "Fired when a new order is placed",
Version: "2025-01-01",
})
r.Endpoints().Create(ctx, endpoint.Input{
TenantID: "tenant-acme",
URL: "https://acme.example.com/webhook",
EventTypes: []string{"order.*"},
})
r.Send(ctx, &event.Event{
Type: "order.created",
TenantID: "tenant-acme",
Data: json.RawMessage(`{"order_id":"ORD-001","amount":99.99}`),
})
r.Start(ctx)
defer r.Stop(ctx)
}