Skip to content

Feature: Webhook Subscription Management #531

Description

@Xhristin3

Context

A webhook subscription can be created (POST /webhooks, api/src/webhooks/webhooks.controller.ts) and its deliveries inspected (GET /webhooks/:id/deliveries), but it can never be changed or stopped. There is no endpoint to list a stream's subscriptions, deactivate or reactivate one, update its URL or event list, or delete it — even though the schema (database/schema.sql) has an active boolean, the repository has findActiveByStreamAndEvent (which filters on it), and the retry sweep already skips inactive subscriptions (WebhooksService.sweepRetries checks !subscription.active). The only way to stop deliveries today is a direct database update.

The consequence for users is operational: a misconfigured webhook (wrong URL, wrong event set) cannot be corrected from the API — the fix is a delete-plus-recreate, which loses the delivery history and requires re-copying a new secret. The maintenance trap a naive fix would create is hardcoding "delete = create again" in the UI, which is exactly what the schema's active flag exists to avoid.

Goal

Expose subscription lifecycle management on the webhook API: list, deactivate/reactivate, update, and delete subscriptions, plus a manual redelivery action for a failed delivery — reusing the existing active flag, repositories, and the retry schedule rather than inventing a parallel mechanism.

Scope

1. Subscription lifecycle endpoints

  • GET /webhooks?streamId= (list the caller's subscriptions, optional stream filter), PATCH /webhooks/:id (URL, events, active), DELETE /webhooks/:id. All ownership-guarded like the existing routes (WebhooksController checks subscription.userId). Deactivation must stop future fan-out (findActiveByStreamAndEvent already enforces it) and stop retries for pending deliveries (sweepRetries already skips inactive subscriptions) — decide whether pending deliveries for a deactivated subscription are left pending, cancelled, or failed, and state it.
  • PATCH must not allow changing the secret; the secret stays creation-time-only (documented as such in WebhooksController).

2. Manual redelivery

  • POST /webhooks/:id/deliveries/:deliveryId/retry (owner-only): resets a failed/pending delivery's retry state (next_attempt_at to now, keep attempt_count or reset per the chosen semantics) so the sweep picks it up. Reuse nextAttemptAfter/MAX_RETRIES from api/src/webhooks/webhooks.service.ts so a manual retry cannot exceed the retry budget silently.

3. Subscriber-facing docs

  • Update the SDK types/docs (xstreamroll-sdk/src/types.ts WebhookSubscription already models active), the SDK README webhook section, and Swagger descriptions so the management surface is discoverable.

Downstream impact

  • api/src/webhooks/webhooks.controller.ts + webhooks.service.ts: new routes and service methods; existing register/listDeliveries unchanged.
  • api/src/webhooks/repository/webhook-subscriptions-db.repository.ts: needs listByUser (and stream filter) and update methods; the in-memory webhook-subscriptions.repository.ts must match for tests.
  • xstreamroll-sdk: optional convenience methods (updateWebhook, deleteWebhook, listWebhooks) and types; SDK README.
  • Contracts: extend tests/contracts with the new endpoints per the existing pattern so provider/consumer suites (api/src/contract-provider.spec.ts, xstreamroll-sdk/__tests__/contract.consumer.test.ts) pin them.

Acceptance criteria

Contract

  • GET /webhooks returns the caller's subscriptions, optionally filtered by streamId, paginated like the other list endpoints.
  • PATCH /webhooks/:id updates URL, events, and/or active; a deactivated subscription stops receiving new events immediately and its pending deliveries are handled per the documented choice.
  • DELETE /webhooks/:id removes the subscription and its deliveries (cascade exists in the schema) and returns 204.
  • POST /webhooks/:id/deliveries/:deliveryId/retry re-queues a failed delivery; a non-owner gets 403; a nonexistent webhook/delivery gets 404.

Service

  • Deactivation does not leak: findActiveByStreamAndEvent (already active = true) is the only fan-out path, and sweepRetries skips inactive subscriptions (already implemented) — verified by tests, not just code reading.

Tests

  • Controller/service specs cover: ownership enforcement on every new route, deactivate-stops-delivery, reactivate-resumes-delivery, and retry re-queues a failed delivery.
  • Contract tests for the new routes pass in both suites.

Documentation

  • Swagger documents all new endpoints; SDK README shows the management flow; the active flag semantics are stated once (controller JSDoc) rather than repeated.

Out of scope

Secret rotation, delivery-signature format changes, and webhook URL SSRF protections (separate issue).

Getting started

Real files in scope: api/src/webhooks/webhooks.controller.ts, api/src/webhooks/webhooks.service.ts, api/src/webhooks/repository/webhook-subscriptions-db.repository.ts, api/src/webhooks/repository/webhook-subscriptions.repository.ts, api/src/webhooks/webhooks.controller.spec.ts, api/src/webhooks/webhooks.service.spec.ts, xstreamroll-sdk/src/types.ts, xstreamroll-sdk/src/client.ts, tests/contracts/src/ (new contract file).

Verify with:

cd api && npm run typecheck && npm test
cd ../xstreamroll-sdk && npm run typecheck && npm test

Good first files to read: api/src/webhooks/webhooks.controller.ts, api/src/webhooks/repository/webhook-subscriptions-db.repository.ts, api/src/webhooks/webhooks.service.ts (sweepRetries).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third CampaignapiREST API design and endpointsenhancementNew feature or requestfeature

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions