Skip to content

feat(webhooks): outbound webhook outbox with dispatcher, retries & admin ops - #75

Open
priscaenoch wants to merge 1 commit into
MyFanss:mainfrom
priscaenoch:feature/54-webhook-outbox
Open

feat(webhooks): outbound webhook outbox with dispatcher, retries & admin ops#75
priscaenoch wants to merge 1 commit into
MyFanss:mainfrom
priscaenoch:feature/54-webhook-outbox

Conversation

@priscaenoch

@priscaenoch priscaenoch commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the transactional outbox described in #54: durable WebhookEvent rows, a dispatcher with exponential backoff, admin inspection, and payload redaction — preparing partner webhooks without a real HTTP delivery client.

closes #54

  • New webhooks module: WebhookEvent entity + migration (indexed on status+nextAttemptAt), WebhooksService (emit/dispatch/retry/listEvents), admin routes under /api/v1/admin/webhook-events (list, dispatch, retry — admin RBAC only, 403 for non-admins), and a deep payload redactor stripping passwords/tokens/secrets/emails from nested objects/arrays.
  • Wired emitters: subscription.created on subscribe (both new subscriptions and reactivating a cancelled one) and post.published on post creation — both now write via dataSource.transaction() so the outbox row commits atomically with the domain write.
  • No cron/scheduler dependency was installed (none exists in this repo); the dispatcher is triggered via POST /api/v1/admin/webhook-events/dispatch, which is also how tests and admins run it on demand. It logs to console by default, or POSTs to WEBHOOK_DEBUG_URL if configured. Failures back off exponentially (capped at 5 min) and go dead after 5 attempts; dead events can be retried via POST .../:id/retry.
  • docs/webhooks-outbox.md documents the data model, emitters, redaction rules, dispatcher behavior, and admin API, including what's intentionally out of scope (signed partner delivery, per-creator webhook management UI).
  • 20 new tests in src/webhooks/ (service, redaction, admin controller) plus emitter-specific tests added to the existing subscriptions.service.spec.ts and posts.service.spec.ts.

Pre-existing baseline breakage (unrelated to #54, fixed to keep CI green)

main currently fails npm run build/test:e2e, apparently from a bad merge of a post-soft-delete-archive-restore feature where the controller/tests/migration landed but the service implementation didn't:

  • Duplicate AuditAction.USER_SELF_DELETED enum member (build-breaking).
  • AuditAction missing several BILLING_WEBHOOK_* members that billing-webhooks.service.ts already referenced.
  • FeedService's post mapping was missing the now-required deletedAt field.
  • PostsService was missing getArchivedPosts/restorePost (and Post was missing deletedById) even though posts.controller.ts, posts.service.spec.ts, and test/posts.e2e-spec.ts already called them — reconstructed faithfully against those existing call sites and the already-present AddPostSoftDelete migration.
  • One e2e assertion (test/posts.e2e-spec.ts) used a numeric user id where every other public-route assertion in that file uses the creator's handle — fixed to match the surrounding pattern.
  • 3 files with pre-existing Prettier violations, reformatted.

I confirmed with the requester before making these fixes, since they're outside #54's scope but were blocking build/test/format:check regardless of this PR's contents.

Testing / validation performed

  • npm run build — clean.
  • npm run format:check — clean.
  • npm test — 49 suites / 380 tests pass.
  • npm run test:e2e (against a local Postgres via Docker, since no DB is available by default) — test/posts.e2e-spec.ts (43 tests) passes in full, including the reconstructed archive/restore endpoints. Did not run the entire e2e/integration suite end-to-end locally; CI's Postgres service should cover the rest.

…& admin ops

Implements a transactional outbox for platform events (issue MyFanss#54): durable
WebhookEvent rows, a dispatcher with exponential backoff, admin inspection,
and payload redaction — preparing partner webhooks without a real HTTP
delivery client.

- New `webhooks` module: WebhookEvent entity/migration (indexed on
  status+nextAttemptAt), WebhooksService (emit/dispatch/retry/list), admin
  routes under /api/v1/admin/webhook-events (list, dispatch, retry — admin
  RBAC only), and a deep payload redactor (passwords/tokens/secrets/emails).
- Wired emitters: `subscription.created` on subscribe (new + reactivate) and
  `post.published` on post creation, both writing the outbox row inside the
  same DB transaction as the domain write via dataSource.transaction().
- Dispatcher has no cron dependency (none installed); it's triggered via the
  admin dispatch endpoint. Logs to console by default, or POSTs to
  WEBHOOK_DEBUG_URL if configured. Failures back off exponentially and go
  `dead` after 5 attempts; dead events can be retried.
- docs/webhooks-outbox.md covers the data model, emitters, redaction,
  dispatcher and admin API. 20 new tests in src/webhooks/, plus new
  emitter-specific tests in subscriptions/posts service specs.

Also fixes pre-existing breakage on main unrelated to this issue, needed to
get build/format/tests green: a duplicate AuditAction.USER_SELF_DELETED
enum member, missing AuditAction members referenced by
billing-webhooks.service.ts, a missing `deletedAt` field in
FeedService's post mapping, and PostsService missing the
getArchivedPosts/restorePost methods (plus post.entity's deletedById
column) that posts.controller.ts, posts.service.spec.ts and
test/posts.e2e-spec.ts already expected from a prior merge. Reformatted
3 previously-unformatted files to pass format:check.
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.

Outbound Webhook Outbox with Dispatcher, Retries & Admin Ops (Hard)

1 participant