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
Service
Tests
Documentation
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).
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 anactiveboolean, the repository hasfindActiveByStreamAndEvent(which filters on it), and the retry sweep already skips inactive subscriptions (WebhooksService.sweepRetrieschecks!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
activeflag 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
activeflag, 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 (WebhooksControllercheckssubscription.userId). Deactivation must stop future fan-out (findActiveByStreamAndEventalready enforces it) and stop retries for pending deliveries (sweepRetriesalready skips inactive subscriptions) — decide whether pending deliveries for a deactivated subscription are left pending, cancelled, or failed, and state it.PATCHmust not allow changing the secret; the secret stays creation-time-only (documented as such inWebhooksController).2. Manual redelivery
POST /webhooks/:id/deliveries/:deliveryId/retry(owner-only): resets a failed/pending delivery's retry state (next_attempt_atto now, keepattempt_countor reset per the chosen semantics) so the sweep picks it up. ReusenextAttemptAfter/MAX_RETRIESfromapi/src/webhooks/webhooks.service.tsso a manual retry cannot exceed the retry budget silently.3. Subscriber-facing docs
xstreamroll-sdk/src/types.tsWebhookSubscriptionalready modelsactive), 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; existingregister/listDeliveriesunchanged.api/src/webhooks/repository/webhook-subscriptions-db.repository.ts: needslistByUser(and stream filter) andupdatemethods; the in-memorywebhook-subscriptions.repository.tsmust match for tests.xstreamroll-sdk: optional convenience methods (updateWebhook,deleteWebhook,listWebhooks) and types; SDK README.tests/contractswith 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 /webhooksreturns the caller's subscriptions, optionally filtered bystreamId, paginated like the other list endpoints.PATCH /webhooks/:idupdates URL, events, and/oractive; a deactivated subscription stops receiving new events immediately and its pending deliveries are handled per the documented choice.DELETE /webhooks/:idremoves the subscription and its deliveries (cascade exists in the schema) and returns 204.POST /webhooks/:id/deliveries/:deliveryId/retryre-queues a failed delivery; a non-owner gets 403; a nonexistent webhook/delivery gets 404.Service
findActiveByStreamAndEvent(alreadyactive = true) is the only fan-out path, andsweepRetriesskips inactive subscriptions (already implemented) — verified by tests, not just code reading.Tests
Documentation
activeflag 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:
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).