sparrow

package module
v1.3.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 1, 2026 License: MIT Imports: 0 Imported by: 0

README

CI Go Docker Docs

Sparrow

Sparrow

Self-hosted webhook delivery platform with async fan-out, retries, health tracking, and observability. Built for teams that need reliable outbound webhooks without depending on a third-party service.

Deploy on Railway

Features

Delivery & Reliability
  • Event-driven fan-out -- push one event, deliver to all matching subscriptions
  • At-least-once delivery -- configurable retries with exponential backoff
  • Idempotent event ingestion -- optional idempotency keys to prevent duplicate processing
  • Per-webhook rate limiting -- leaky bucket algorithm with HTTP 429 Retry-After parsing
  • 10-category error classification -- DNS, TLS, timeout, connection refused, rate limited, and more -- each with retryability flags
  • Bulk operations -- deterministic snapshot-based batch re-push and retry (up to 10K items)
Security
  • Dual webhook signing -- every delivery is signed with both HMAC-SHA256 and Ed25519 (Standard Webhooks format)
  • Envelope encryption at rest -- AES-256-GCM with per-record data encryption keys for webhook secrets and sensitive headers
  • SSRF protection -- blocks private/loopback/metadata IPs, validates redirect targets
  • Optional API key auth -- constant-time comparison, HTTP + gRPC support
Developer Experience
  • Payload transformation -- Go templates per subscription (50+ functions) to reshape payloads for different consumers
  • Soft schema validation -- warnings not errors; events are always accepted and stored
  • Dual-protocol API -- gRPC on :50051 and Connect-RPC (HTTP/JSON) on :8080
  • Web dashboard -- embedded SvelteKit UI for managing webhooks, events, deliveries, and health
  • Health tracking -- per-webhook state machine (healthy/degraded/unhealthy) with rolling summaries
Operations
  • PostgreSQL only -- no Redis, no message broker, no external dependencies beyond one database
  • OpenTelemetry -- traces, metrics, and structured logs with job-level trace propagation
  • Helm chart -- security-hardened Kubernetes deployment (NetworkPolicy, read-only rootfs, non-root, seccomp)
  • One-click deploy -- Railway, Docker Compose, or any container platform

Quick Start

Download deploy/docker-compose.yml and start it:

curl -O https://raw.githubusercontent.com/sarathsp06/sparrow/main/deploy/docker-compose.yml
SPARROW_ENCRYPTION_KEY=$(openssl rand -hex 32) docker compose up -d

Open http://localhost:8080 for the web UI.

Send your first event
# Register an event type
curl -X POST http://localhost:8080/webhook.EventService/RegisterEvent \
  -H "Content-Type: application/json" \
  -d '{"name": "order.created", "description": "New order", "active": true}'

# Register a webhook (subscription is created automatically)
curl -X POST http://localhost:8080/webhook.WebhookService/RegisterWebhook \
  -H "Content-Type: application/json" \
  -d '{"namespace": "default", "url": "https://httpbin.org/post", "events": ["order.created"], "active": true}'

# Push an event -- Sparrow fans out and delivers
curl -X POST http://localhost:8080/webhook.EventService/PushEvent \
  -H "Content-Type: application/json" \
  -d '{"namespace": "default", "event": "order.created", "payload": {"order_id": "ord_123", "amount": 99.99}}'

Check delivery status in the web UI at Deliveries, or query the API:

curl -X POST http://localhost:8080/webhook.DeliveryService/ListDeliveries \
  -H "Content-Type: application/json" \
  -d '{"namespace": "default", "limit": 5}'

Use Cases

  • SaaS webhook notifications -- notify customer endpoints when resources change
  • Internal event bus -- fan out domain events to downstream services over HTTP
  • Reliability layer -- add retries, health tracking, and observability to existing webhook flows
  • Development and testing -- inspect deliveries, replay failed events, test payload transforms

Architecture

PushEvent API
  -> persist event in PostgreSQL
  -> enqueue fan-out job (River)
     -> match subscriptions, apply transforms, create deliveries
     -> enqueue delivery jobs
        -> HTTP POST with HMAC signature
        -> retry on failure (server errors, timeouts, network errors)
        -> track health per webhook

Events are persisted before delivery. The River job queue provides at-least-once delivery with configurable retries (default: 3 attempts, 60s backoff). Failures are classified into 10 categories -- retryable (5xx, timeout, connection refused, network error, rate limited) and non-retryable (4xx, DNS, TLS) -- so you know why a delivery failed, not just that it failed. Every delivery is dual-signed with HMAC-SHA256 and Ed25519 using the Standard Webhooks format. Webhook secrets and sensitive headers are envelope-encrypted at rest using AES-256-GCM with per-record data encryption keys.

See the architecture reference for the full pipeline design, error classification, and health state machine.

Verifying Webhook Signatures

Every delivery includes three Standard Webhooks headers:

Header Example
webhook-id msg_abc123-def456
webhook-timestamp 1716048000 (Unix seconds)
webhook-signature v1,K7gNU3sdo+OL... or v1a,RjB2mN...

The signed message is always: {webhook-id}.{webhook-timestamp}.{raw request body} -- the exact bytes of the JSON body, not re-serialized.

The webhook-signature prefix tells you which algorithm was used:

  • v1, -- HMAC-SHA256 (symmetric, requires the shared webhook secret)
  • v1a, -- Ed25519 (asymmetric, requires only the public key from the API)
Verifying HMAC-SHA256 (v1,)
import hmac, hashlib, base64

def verify_hmac(body: bytes, secret: str, headers: dict) -> bool:
    msg_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]
    signature = headers["webhook-signature"]  # "v1,<base64>"

    # Extract the base64 portion after "v1,"
    expected_b64 = signature.removeprefix("v1,")

    # Reconstruct the signed message
    message = f"{msg_id}.{timestamp}.{body.decode()}"

    # Compute HMAC-SHA256
    computed = hmac.new(secret.encode(), message.encode(), hashlib.sha256).digest()
    computed_b64 = base64.b64encode(computed).decode()

    return hmac.compare_digest(computed_b64, expected_b64)
func VerifyHMAC(body []byte, secret, msgID, timestamp, signatureHeader string) bool {
    // Extract "v1,<base64>" -> "<base64>"
    b64Sig, _ := strings.CutPrefix(signatureHeader, "v1,")

    message := msgID + "." + timestamp + "." + string(body)
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(message))
    expected := base64.StdEncoding.EncodeToString(mac.Sum(nil))

    return hmac.Equal([]byte(expected), []byte(b64Sig))
}
Verifying Ed25519 (v1a,)

The public key is returned in the signing_public_key field when you register or retrieve a webhook.

import base64
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

def verify_ed25519(body: bytes, public_key_b64: str, headers: dict) -> bool:
    msg_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]
    signature = headers["webhook-signature"]  # "v1a,<base64>"

    sig_bytes = base64.b64decode(signature.removeprefix("v1a,"))
    pub_key = Ed25519PublicKey.from_public_bytes(base64.b64decode(public_key_b64))

    message = f"{msg_id}.{timestamp}.{body.decode()}".encode()

    try:
        pub_key.verify(sig_bytes, message)
        return True
    except Exception:
        return False
func VerifyEd25519(body []byte, publicKeyB64, msgID, timestamp, signatureHeader string) bool {
    b64Sig, _ := strings.CutPrefix(signatureHeader, "v1a,")
    sig, _ := base64.StdEncoding.DecodeString(b64Sig)
    pubKey, _ := base64.StdEncoding.DecodeString(publicKeyB64)

    message := []byte(msgID + "." + timestamp + "." + string(body))
    return ed25519.Verify(ed25519.PublicKey(pubKey), message, sig)
}
Replay Protection

Always validate the webhook-timestamp header to prevent replay attacks. Reject deliveries where the timestamp is more than 5 minutes from your server's current time.

Configuration

All configuration is via environment variables:

Variable Required Default Description
DATABASE_URL Yes -- PostgreSQL connection string
SPARROW_SERVE_UI No false Serve the embedded web dashboard
SPARROW_API_KEY No -- Require this key in X-API-Key header
SPARROW_ENCRYPTION_KEY Yes -- 64-char hex key for envelope encryption of secrets. Generate with openssl rand -hex 32
OTEL_EXPORTER_OTLP_ENDPOINT No -- OTLP endpoint for traces/metrics/logs

See the configuration reference for the full list.

Deployment

Railway

Deploy on Railway

Deploy Sparrow + PostgreSQL on Railway with zero infrastructure. See the Railway deployment guide for setup steps.

Docker

Pre-built multi-arch images (linux/amd64, linux/arm64) are published on every release:

docker pull ghcr.io/sarathsp06/sparrow:latest

See Docker Compose deployment guide for details.

Kubernetes

A Helm chart is included at charts/sparrow/:

helm install sparrow charts/sparrow/ \
  --set secrets.databaseURL="postgres://user:pass@your-db:5432/sparrow?sslmode=require"

See Kubernetes deployment guide for all chart values and examples.

Documentation

sarathsp06.github.io/sparrow

API reference docs are generated from webhook.proto using proto2astro.

In-repo docs: Configuration | Architecture | Kubernetes | webhook.proto

Contributing

Contributions are welcome. Please open an issue to discuss larger changes before submitting a PR.

git clone https://github.com/sarathsp06/sparrow.git
cd sparrow
make build-with-ui   # build server + embedded UI
make test            # run tests
make lint            # run linters

See the architecture reference for the package structure and dependency graph.

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

View Source
var Version = "-NOVERSION-"

Version is set at build time via -ldflags "-X github.com/sarathsp06/sparrow.Version=..." Defaults to "-NOVERSION-" when built without ldflags (e.g. `go run`).

Functions

This section is empty.

Types

This section is empty.

Directories

Path Synopsis
cmd
benchmark command
migrate command
server command
internal
config
Package config provides structured configuration loading from environment variables using kelseyhightower/envconfig.
Package config provides structured configuration loading from environment variables using kelseyhightower/envconfig.
health
Package health provides health checking functionality for the Sparrow webhook service
Package health provides health checking functionality for the Sparrow webhook service
middleware
Package middleware provides HTTP and gRPC middleware for the Sparrow server.
Package middleware provides HTTP and gRPC middleware for the Sparrow server.
ui
Package ui provides an embedded SPA file server for the Sparrow web frontend.
Package ui provides an embedded SPA file server for the Sparrow web frontend.
pkg
crypto
Package crypto provides envelope encryption (AES-256-GCM, per-record DEK) for sensitive data such as secret headers and webhook secrets.
Package crypto provides envelope encryption (AES-256-GCM, per-record DEK) for sensitive data such as secret headers and webhook secrets.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL