Self-hosted webhook delivery service. Send events, Pigeon delivers them.
You send a message with an event type and a JSON payload. Pigeon fans it out to all endpoints subscribed to that event type, signs each delivery with HMAC-SHA256, retries on failure with exponential backoff, and dead-letters after exhaustion. Everything is scoped per organization via OIDC-based multitenancy.
docker pull ghcr.io/the127/pigeon-server:1
docker pull ghcr.io/the127/pigeon-ui:1
# Minimal: API + worker + Postgres
docker run -d --name pigeon \
-e DATABASE_URL=postgres://user:pass@host/pigeon \
-e PIGEON_BOOTSTRAP_ORG_ENABLED=true \
-e PIGEON_BOOTSTRAP_OIDC_ISSUER_URL=https://your-oidc-provider \
-e PIGEON_BOOTSTRAP_OIDC_AUDIENCE=pigeon-api \
-p 3000:3000 \
ghcr.io/the127/pigeon-server:1The server runs migrations on startup. No separate step needed.
Hexagonal / ports-and-adapters. Rust workspace with compiler-enforced crate boundaries.
pigeon-macros proc macros (Reconstitute derive)
pigeon-domain aggregates, value objects, domain events
pigeon-application commands, queries, ports, mediator pipeline
pigeon-infrastructure Postgres adapters, HTTP webhook client
pigeon-api axum handlers, JWT auth, OpenAPI
pigeon-server composition root, CLI
pigeon-ui Vue 3 + TypeScript frontend
Organization (tenant)
└── OidcConfig
└── Applications
├── EventTypes
├── Endpoints → signing_secrets (Pigeon-generated, rotatable)
└── Messages → Attempts → DeadLetters
All endpoints require JWT Bearer auth. org_id is resolved from the token, never from the URL.
| Resource | Endpoints |
|---|---|
| Applications | CRUD + stats |
| Event Types | CRUD + stats |
| Endpoints | CRUD + stats + rotate/revoke signing secrets + test event |
| Messages | Send (idempotent) + list + retrigger |
| Attempts | List by message + retry failed |
| Dead Letters | List + replay |
| OIDC Configs | List + create + delete (own org) |
| Audit Log | List with filters |
Organization and OIDC config management. Requires bootstrap org JWT.
GET /api/openapi.json— OpenAPI 3.0 specGET /health/GET /health/ready— liveness / readinessGET /metrics— Prometheus metrics
- Fan-out: message → one attempt per subscribed endpoint
- Signing: HMAC-SHA256 with all active signing secrets. Header:
X-Pigeon-Signature: sha256=<sig1>,sha256=<sig2> - Retry: exponential backoff (configurable base/max),
SELECT ... FOR UPDATE SKIP LOCKED - Dead letter: after max retries, attempts are dead-lettered. Replayable via API.
- Auto-disable: endpoints with consecutive failures above threshold are automatically disabled
- Idempotency: messages keyed by
idempotency_key, duplicates return existing message
Pigeon generates signing secrets (whsec_ + 64 hex chars). Endpoints support up to 2 active secrets for zero-downtime rotation:
POST .../endpoints/{id}/rotate— generates new secret, keeps old- Pigeon signs deliveries with all active secrets
- Update your consumer to verify with the new secret
DELETE .../endpoints/{id}/secrets/1— revoke the old secret
Full secrets are shown only on create and rotate. Masked (whsec_...abc123) everywhere else.
All via environment variables. PIGEON_ prefix for app config.
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
required | Postgres connection string |
PIGEON_LISTEN_ADDR |
0.0.0.0:3000 |
API listen address |
PIGEON_BOOTSTRAP_ORG_ENABLED |
false |
Create bootstrap org on startup |
PIGEON_BOOTSTRAP_ORG_NAME |
System |
Bootstrap org display name |
PIGEON_BOOTSTRAP_ORG_SLUG |
system |
Bootstrap org URL slug |
PIGEON_BOOTSTRAP_OIDC_ISSUER_URL |
— | Required if bootstrap enabled |
PIGEON_BOOTSTRAP_OIDC_AUDIENCE |
— | Required if bootstrap enabled |
PIGEON_BOOTSTRAP_OIDC_JWKS_URL |
derived from issuer | JWKS endpoint override |
PIGEON_JWKS_CACHE_TTL_SECS |
3600 |
JWKS cache TTL |
PIGEON_WORKER_BATCH_SIZE |
10 |
Attempts dequeued per poll |
PIGEON_WORKER_POLL_INTERVAL_MS |
1000 |
Idle poll interval |
PIGEON_WORKER_MAX_RETRIES |
5 |
Attempts before dead letter |
PIGEON_WORKER_BACKOFF_BASE_SECS |
30 |
Exponential backoff base |
PIGEON_WORKER_MAX_BACKOFF_SECS |
3600 |
Backoff cap |
PIGEON_WORKER_HTTP_TIMEOUT_SECS |
30 |
Webhook request timeout |
PIGEON_WORKER_AUTO_DISABLE_THRESHOLD |
5 |
Consecutive dead letters before auto-disable (0 = off) |
PIGEON_WORKER_CLEANUP_INTERVAL_SECS |
3600 |
Idempotency key cleanup interval |
The server binary supports multiple subcommands:
pigeon serve # API + worker (default in Docker)
pigeon api # API only
pigeon worker # Worker only
pigeon migrate # Run migrations and exit# Prerequisites: Rust, Node.js, Docker (for Postgres)
# Start Postgres
just dev-up
# Run migrations + start server
just dev-run
# Frontend
cd pigeon-ui && npm install && npm run dev
# Tests
just test # all tests
just clippy # lint
just ci # fmt + clippy + test
# Generate OpenAPI client (requires running server)
cd pigeon-ui && npm run generate-apiAGPL-3.0