03 - User Journey (Webhooks Module)
- Journey 1 — Register and verify a webhook (Tenant Developer)
- Journey 2 — Event arrives, delivery fails, manual retry
- Journey 3 — Pause, edit, delete (School Admin)
- Journey 4 — External vendor pushes events into StudyLyon
(planned) - Journey map (mermaid)
End-to-end journeys against the implemented backend. Wire contracts quoted from
src/modules/webhooks/**. Journeys 1-2 are implemented; journey 4 is(planned)(inbound receiver).
Journey 1 — Register and verify a webhook (Tenant Developer)
- Developer signs in (web-first surface, post-Phase 1 per
PRODUCT_REQUIREMENTS_DOCUMENT.md:144). - Opens Webhooks list -
GET /api/v1/webhooks(sort newest first,webhooks.service.ts:35-37) - sees existing subscriptions and their enabled/paused state. - Taps "New webhook" -
POST /api/v1/webhooks(webhooks.controller.ts:21-25) with{ name, url, events[1..n], secret, enabled? }(create-webhook.dto.ts:11-34). URL must be valid (@IsUrl({ require_tld: false }),create-webhook.dto.ts:17-18); events array must have at least one entry (ArrayMinSize(1),create-webhook.dto.ts:22-23). - Fires the test ping -
POST /api/v1/webhooks/:id/test(webhooks.controller.ts:65-70) - the worker POSTs{ test: true, webhookId }as eventWebhookTested(webhooks.service.ts:130-138). - Receiver gets
X-Webhook-Signature: <hmac-sha256 hex>+X-Webhook-Event: WebhookTested(webhook-delivery.worker.ts:70-82); developer verifies signature with the stored secret. - Watches
GET /api/v1/webhooks/:id/logs(50 latest,webhooks.service.ts:88-93) andGET /api/v1/webhooks/:id/metrics(webhooks.service.ts:142-156) -total/success/failed/pendingcounts. - Done when the log row shows
status: success(recordDelivery,webhooks.service.ts:179-193).
Journey 2 — Event arrives, delivery fails, manual retry
- A domain event is emitted on the
EventBus(event-bus.service.ts:11-14). WebhooksServiceintercepts viaonAny(webhooks.service.ts:26-28) and queries active subscriptions for that event type - tenantId + enabled + not deleted +events: eventType(webhook.repository.ts:17-29).- One job per matching webhook is enqueued:
deliveron queuewebhook-deliver(queue.constants.ts:14) withattempts: 3and exponential backoff starting at 5 s (webhooks.service.ts:69-84). - Worker creates a
pendingdelivery log (webhooks.service.ts:163-177), POSTs the payload (webhook-delivery.worker.ts:73-82), and recordssuccesson 2xx elsefailed+ rethrows (webhook-delivery.worker.ts:84-100); BullMQ retries. - All attempts exhausted - developer sees
failedin logs and red in metrics. - Developer taps "Retry" -
POST /api/v1/webhooks/:id/retry(webhooks.controller.ts:58-63) - the latest failed delivery is re-enqueued with its original payload (webhooks.service.ts:95-120). - Retry succeeds - log row flips to
success(new row; the failed row stays - one log row per worker attempt,webhooks.service.ts:62-67, 84-89).
Journey 3 — Pause, edit, delete (School Admin)
- Admin pauses a misbehaving integration -
POST /api/v1/webhooks/:id/pause(webhooks.controller.ts:78-83) - fan-out now skips it (webhook.repository.ts:23-28). - Later resumes -
POST /api/v1/webhooks/:id/resume(webhooks.controller.ts:85-90). - Developer edits URL/events/secret -
PATCH /api/v1/webhooks/:idwith any subset ofUpdateWebhookDto(PartialType,update-webhook.dto.ts:4). - Decommission -
DELETE /api/v1/webhooks/:id- soft delete (webhooks.service.ts:50-53,base.repository.ts:68-74); list no longer shows it.
Journey 4 — External vendor pushes events into StudyLyon (planned)
- Vendor signs up for the planned unauthenticated
publicscope (IMPLEMENTATION_PLAN.md:48- "(webhooks, health)", 30 req/min, 1 min window). - Vendor POSTs events to the public receiver - no such endpoint exists in
source; blocked by
IMPLEMENTATION_PLAN.md:856(test-series integration via webhooks). Marked(planned); excluded from all API tables in 12. - Journey ends here until the receiver is implemented.
Journey map (mermaid)
flowchart LR
A[Domain event emitted<br/>event-bus.service.ts:11-14] --> B[onAny fan-out<br/>webhooks.service.ts:26-28]
B --> C[findActiveByEvent<br/>webhook.repository.ts:17-29]
C -->|match| D[enqueue deliver job<br/>attempts 3, backoff 5s<br/>webhooks.service.ts:80-83]
D --> E[worker: pending log<br/>webhooks.service.ts:163-177]
E --> F[POST url + HMAC sig<br/>webhook-delivery.worker.ts:70-82]
F -->|2xx| G[log success]
F -->|non-2xx/timeout| H[log failed + rethrow<br/>:84-100]
H -->|attempts left| D
H -->|exhausted| I[manual retry<br/>POST /:id/retry]
I --> D