Skip to content

feat: Webhooks support#16

Merged
xernobyl merged 61 commits intomainfrom
feat/webhooks
Feb 10, 2026
Merged

feat: Webhooks support#16
xernobyl merged 61 commits intomainfrom
feat/webhooks

Conversation

@xernobyl
Copy link
Contributor

@xernobyl xernobyl commented Feb 5, 2026

Adds Standard Webhooks support for Hub events, based on River as a queue.

Event / Webhooks pipeline (architecture)

  1. API request – Client calls a mutation (e.g. POST /v1/feedback-records, DELETE /v1/webhooks/{id}). The handler calls the corresponding service.
  2. Service publishes event – The service persists the change, then calls MessagePublisherManager with event type and payload (e.g. PublishEvent(FeedbackRecordCreated, record)). This is fire-and-forget into an in-memory channel; the API does not wait for webhook delivery.
  3. MessagePublisherManager fans out – A background goroutine reads from the channel and calls PublishEvent on each registered provider (today: only WebhookProvider). Per-event timeout is configurable via MESSAGE_PUBLISHER_PER_EVENT_TIMEOUT_SECONDS (default 10s) so one provider doesn’t block others. If the channel is full, the event is dropped and a warning is logged. Channel buffer size is configurable via MESSAGE_PUBLISHER_BUFFER_SIZE (default 1024).
  4. WebhookProvider enqueues jobs – For each event, it queries the DB for enabled webhooks that subscribe to that event type, builds a batch of job params (capped by WEBHOOK_MAX_FAN_OUT_PER_EVENT, default 500), and calls River’s InsertMany once. No HTTP here; only list + enqueue. Jobs are unique by (event id, webhook id) for 24 hours.
  5. River runs the worker – River picks up jobs and runs WebhookDispatchWorker for each. The worker loads the webhook by ID, builds the payload, and calls WebhookSender.
  6. WebhookSender delivers – Signs the payload (Standard Webhooks), sets headers (webhook-id, webhook-signature, webhook-timestamp), POSTs to the webhook URL. 2xx → job complete. 410 Gone → webhook disabled in DB, no retry. Other failures → retry with backoff until max attempts; after last failure the webhook can be disabled.

In short: API → Service → MessagePublisherManager (channel) → WebhookProvider (DB + River InsertMany) → River queue → WebhookDispatchWorker → WebhookSender → HTTP POST. The slow/unreliable part (HTTP to third-party URLs) stays in the worker and sender; the API stays non-blocking.

What’s in this branch

Webhooks

  • CRUD API for webhook endpoints: POST/GET/PATCH/DELETE /v1/webhooks, with list/filter support.
  • Event types: feedback_record.created, feedback_record.updated, feedback_record.deleted (and webhook lifecycle events).
  • Delivery: River-backed worker sends signed payloads (Standard Webhooks) to subscriber URLs with retries and configurable concurrency/attempts.
  • Batch enqueue: Provider uses River’s InsertMany (one call per event) and a configurable fan-out cap per event (WEBHOOK_MAX_FAN_OUT_PER_EVENT, default 500) to avoid blocking the publisher when an event has many webhooks.
  • Migrations: 002_webhooks.sql, 003_webhook_disabled_state.sql (and dependency on goose migrations).
  • OpenAPI: New webhook schemas and operations in openapi.yaml.

Infrastructure & config

  • River: River client and WebhookDispatchWorker in cmd/api/main.go; webhook provider enqueues jobs via InsertMany (one batch per event, capped by WEBHOOK_MAX_FAN_OUT_PER_EVENT).
  • Docker Compose: riverui service for the River UI (port 8081), with basic auth via RIVER_BASIC_AUTH_USER / RIVER_BASIC_AUTH_PASS (no defaults in compose).
  • Config: WEBHOOK_DELIVERY_MAX_CONCURRENT, WEBHOOK_DELIVERY_MAX_ATTEMPTS, WEBHOOK_MAX_FAN_OUT_PER_EVENT; MESSAGE_PUBLISHER_BUFFER_SIZE, MESSAGE_PUBLISHER_PER_EVENT_TIMEOUT_SECONDS, SHUTDOWN_TIMEOUT_SECONDS; .env.example updated with River UI auth and the above vars.

Other

  • Message publisher: Event publishing abstraction used by the feedback-records service to trigger webhook delivery; buffer size and per-event timeout configurable.
  • Integration tests: Webhook CRUD and auth covered in tests/integration_test.go.

How to try it

  1. make init-db and make river-migrate.
  2. docker compose up -d for Postgres + River UI (set River UI auth in .env).
  3. Create a webhook (e.g. feedback_record.created) and create a feedback record; confirm delivery in River UI and at the webhook URL.

xernobyl and others added 30 commits February 2, 2026 14:26
- Add Event and MessagePublisher interface with MessagePublisherManager
- Add event type enum (internal/datatypes/event_type.go) for feedback_record
  and future webhook event types
- Add placeholder EmailDeliveryService implementing MessagePublisher
- Wire FeedbackRecordsService to publish events on create/update/delete
- Wire message manager in main and integration tests (no webhook provider yet)

Co-authored-by: Cursor <cursoragent@cursor.com>
- Use goose for app schema (migrations/001, 002_webhooks)
- Single 002_webhooks.sql migration with full webhooks table and indexes
- Keep river-migrate target and add to CI after init-db
- AGENTS.md: document river-migrate for webhook delivery

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Contributor

@BhagyaAmarasinghe BhagyaAmarasinghe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

Copy link
Contributor

@BhagyaAmarasinghe BhagyaAmarasinghe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR :)
I have added several comments that needs to be looked.

@xernobyl xernobyl enabled auto-merge February 10, 2026 14:03
Copy link
Contributor

@BhagyaAmarasinghe BhagyaAmarasinghe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, LGTM 🚀

@xernobyl xernobyl added this pull request to the merge queue Feb 10, 2026
Merged via the queue into main with commit b183fd7 Feb 10, 2026
6 checks passed
@xernobyl xernobyl deleted the feat/webhooks branch February 10, 2026 19:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants