# Rails Webhooks

> Build and review Rails webhook systems with safe delivery, retries, observability, and tenant-aware security controls. Use when adding webhook endpoints, outbound deliveries, retry logic, or webhook admin tooling.

- Skill: `marckohlbrugge/rails-webhooks` (Agent Skill)
- Install (CLI): `npx skillmds@latest add marckohlbrugge/rails-webhooks`
- Raw SKILL.md: https://api.skillmd.com/api/skills/marckohlbrugge/rails-webhooks/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: marckohlbrugge (https://skillmd.com/u/marckohlbrugge)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/marckohlbrugge/rails-webhooks

---


# Rails Webhooks

Use for outbound/inbound webhook architecture and reliability. Modeled on Fizzy's event-driven webhook pipeline (with Campfire's simpler bot webhooks as contrast).

## Delivery Model (outbox pattern)

- Domain events enqueue one dispatch job; the dispatch job fans out, creating one persisted `Delivery` row per webhook, each with a state enum (`pending in_progress completed errored` — string-backed via `index_by(&:itself)`).
- Each delivery row auto-enqueues its own send via `after_create_commit :deliver_later` — persist first, deliver second, so state survives crashes and retries.
- Fan-out is crash-safe with `ActiveJob::Continuable`: cursor over matching webhooks (`find_each(start: step.cursor)` + `step.advance!`) so a mid-batch crash resumes, not restarts.
- Record request metadata (headers, payload) and response (status, body) on the delivery row for audit/debugging. Cap stored/streamed response bodies (~100KB) with a running byte count.
- Use a dedicated `webhooks` queue so slow destinations can't starve other work.

## Failure Classification (the key distinction)

- **Expected destination failures** (timeout, TLS, DNS, connection refused, HTTP 4xx/5xx): rescue, mark delivery `completed` with a symbolic error (`response: { error: :connection_timeout }`). The delivery ran; the destination failed. Do not retry the job.
- **Unexpected exceptions** (our bug): mark `errored!`, re-raise so ActiveJob retries.
- This split keeps retry behavior, dashboards, and delinquency tracking honest.

## Delinquency Circuit Breaker

- Track consecutive failures + `first_failure_at` per webhook; auto-deactivate after N failures spanning a minimum window (Fizzy: 10 failures over 1+ hour).
- Reset the counter on success.
- Surface inactive state in the UI with a manual reactivation endpoint (`resource :activation`).

## Security Baseline

- Treat webhook URLs as untrusted input; apply full SSRF protections (resolve + validate IP, block private ranges, pin IP, re-validate per redirect — see rails-security-multitenancy).
- Revalidate destination at send time, not just on create.
- **Destination URL is immutable after create** — updates permit name/subscriptions only. Retargeting requires a new webhook (and new secret).
- Sign payloads: HMAC signature header + timestamp header.
- Whitelist subscribable events at the model layer: `normalizes :subscribed_actions, with: ->(v) { Array.wrap(v).map(&:to_s).uniq & PERMITTED_ACTIONS }`.
- Require admin-level auth for webhook management endpoints.

## Integration Adapters

- One delivery pipeline can serve multiple destination types: detect by URL pattern (`for_slack?`, `for_campfire?`), then vary content type, payload format (JSON/form/HTML), and rendering template per destination. Don't fork the delivery code per integration.
- Render payload URLs with tenant-correct `script_name` so links in payloads work.

## Inbound Webhooks (receiving)

- Verify the signature first (`construct_event`-style), then re-fetch canonical state from the provider's API instead of trusting payload content or ordering.
- For chat-bot style callbacks: gate what responses can do by content type (only `text/plain`/`text/html` become replies), and prevent loops — never trigger a bot from its own messages.

## Tenant Safety

- Keep webhook records and delivery queries tenant-scoped.
- Ensure event fan-out cannot leak cross-tenant data.

## Operational Hygiene

- Recurring cleanup of old delivery records by retention policy (e.g. every 4 hours, `delete_all` on a stale scope).
- Surface delivery status/history in the webhook admin UI.
- Emit useful logs/metrics for success rate, retries, and latency.

## Red Flags

- Fire-and-forget delivery with no persisted audit trail.
- Retrying destination failures the same way as code errors.
- Mutable webhook destination URLs.
- No circuit breaker — hammering dead endpoints forever.
- Unbounded reads of destination responses.
- No tenant scoping in delivery creation/lookup.
- No backpressure or queue isolation for high-volume events.

