add-webhook-provider
Accept webhooks from a new third party (a payment, email or other provider calling us) in apps/webhooks, verified and processed from a queue. Use when a provider needs to notify the product of events.
SKILL.md.claude/skills/add-webhook-provider/SKILL.md
GitHubAdd an inbound webhook provider
Inbound webhooks follow one shape (apps/webhooks/src/inbound/stalwart.routes.ts is the
reference): verify on the raw body, store once, emit a
domain event in the same transaction, answer at once, and do the work from a queue.
- Secret.
<PROVIDER>_WEBHOOK_SECRETinapps/webhooks/src/env.ts, optional (the route answers 404 until it's set), plus.env.example,docs/environment.mdand the webhooks Secret (the add-env-var skill). Accept a comma-separated list if the provider supports rotating secrets. - Pure part.
src/inbound/<provider>-events.ts: the payload's zod schema, signature check withtimingSafeEqual, freshness (a timestamp window, against replays), and the key that identifies an event once (the provider's id, or a hash of its content when ids aren't stable). Unit-test it against real payloads from the provider's docs (<provider>-events.test.ts, likestalwart-events.test.ts). - Route.
src/inbound/<provider>.routes.tswithmount<Provider>(fastify, database, secret): a raw-body content-type parser scoped to the route, the checks above, then one transaction that inserts intowebhooks.inbound_event(unique onproviderandproviderEventId, so a retry is a no-op) andemitEvent(tx, "<provider>.event_received.v1", ...). Mount it insrc/server.ts. - Event. Declare
<provider>.event_received.v1inpackages/contracts/src/events.ts(additive), its audit labelworkspace.audit.events.<provider>.event_received.v1inpackages/i18n/messages/en.json, and a route to its consumer ineventSubscribers(packages/jobs/src/queues.ts). - Consumer. In the service that owns the effect (as apps/notifications consumes the
mail server's bounces): idempotent,
parseJobfirst (the add-job skill). - Reachability.
/webhooksalready routes to the service (deploy/charts/stack/values.yaml); a provider calling from inside the cluster needs its namespace inallowFromNamespaces. Register the endpoint URL (https://<site>/webhooks/<provider>) at the provider, and add that step todocs/new-project.md. - Tests.
apps/webhooks/test/webhooks.integration.test.ts: a signed request is stored once and emits its event, a bad signature and a stale one are refused, a retried delivery is harmless. The consumer's own integration test for its effect.
Done when
bun run testandbun run test:integrationpass.- The
security-revieweragent (a new unauthenticated entry point) and therevieweragent report nothing blocking.