Webhook delivery infrastructure. Register a destination endpoint, push events over the API, and Relay signs each delivery with HMAC-SHA256, retries failures with exponential backoff, dead-letters what won't deliver, and lets you replay any of it from a dashboard. Multi-tenant SaaS with session + API-token auth and Stripe billing.
Portfolio project — a real, working MVP, not a toy. The reliability core (signing, retry/backoff, idempotency, dead-letter, replay) is genuinely implemented and tested.
- Sign — every delivery carries
X-Relay-Signature: sha256=<HMAC>over"{timestamp}.{body}"with the endpoint's secret, so the destination can verify authenticity and reject replays. - Retry with backoff — failed deliveries retry up to 5 times with exponential backoff
(0, 2, 4, 8, 16s), each attempt logged with its
next_retry_at. - Dead-letter — once attempts are exhausted the event is marked dead-lettered.
- Replay — a manual action re-runs delivery from a clean slate when the destination is healthy again.
- Idempotent ingestion — an
Idempotency-Keyheader dedupes retried sends.
| Layer | Choice |
|---|---|
| Framework | PHP 8.4 · Laravel 12 |
| Frontend | Inertia 2 · React 19 · Tailwind · shadcn/ui (Laravel React starter kit) |
| Queue / worker | Laravel Horizon + Redis (DeliverWebhook jobs) |
| Data | PostgreSQL 18 · Redis |
| Auth | Session (dashboard) · Sanctum bearer tokens (ingestion API) |
| Billing | Laravel Cashier (Stripe, test mode) — Billable on the Tenant |
| Infra | Laravel Sail · ./relay CLI · Pint · PHPUnit |
Row-level multi-tenancy (tenant_id FK, a BelongsToTenant global scope).
dispatch ┌───────┐
client ─POST─▶ Laravel app ──▶ │ redis │
│ job └───┬───┘
│ inserts │ pop
┌─────▼─────┐ ┌─────▼─────┐ signed POST
│ pgsql │◀─────│ horizon │ ───────────▶ your endpoint
└───────────┘ log └───────────┘ retry/backoff/dead-letter
every attempt
dashboard (Inertia/React) ─▶ app (only the app is exposed to the host)
Full design — data model, retry algorithm, signing scheme, idempotency — is in
docs/decisions/0001-architecture.md.
git clone https://github.com/guiwohl/relay && cd relay
cp .env.example .env
./relay artisan key:generate # or paste an APP_KEY
./relay init # build, boot, install deps, build assets, migrate + seedOpen the dashboard at http://localhost:8081 (change with APP_PORT in .env).
Horizon is at /horizon. The seeder creates demo@relay.test / password. Only the app
is exposed to the host; pgsql/redis live on the internal Docker network. For a stable
URL, point portless at the app container to get
https://relay.localhost.
Common commands: ./relay horizon · ./relay test · ./relay lint · ./relay tinker ·
./relay down. Run ./relay with no args for the full list.
The end-to-end retry/replay demo is scripted in
docs/runbooks/local-dev.md.
Billing code is real (Cashier Checkout Session + signature-verified webhook). The live
demo needs your own Stripe test-mode keys in .env (STRIPE_KEY, STRIPE_SECRET,
STRIPE_PRICE_ID, STRIPE_WEBHOOK_SECRET). Automated tests don't call Stripe for
checkout but exercise the webhook signature path for real, so ./relay test needs no keys.
./relay test runs the PHPUnit suite (SQLite in-memory — no services needed): signing is
verifiable by a destination, backoff schedule + cap, retry→dead-letter, replay recovery,
idempotency dedupe, auth + tenant isolation, and Cashier webhook signature verification
(valid signature mutates state; tampered is rejected).