Webhooks Module (Clerk)
This document describes the current Clerk webhooks module architecture in apps/api/src/modules/webhooks/clerk-webhooks.
Purpose
The Clerk webhooks module:
- receives signed Clerk webhook requests
- verifies signature at the HTTP adapter boundary
- maps verified events into application-level handling
- synchronizes user registration for
user.createdevents - returns a normalized acknowledgment payload for verified events
Module structure
| Path | Role |
|---|---|
composition.ts | Wires route adapter and use-case with injected user registration dependency |
adapters/http/routes.ts | Fastify route, raw body setup, signature verification, response mapping |
application/use-cases/handle-clerk-webhook.ts | Orchestrates webhook handling with ports/domain entity |
application/dtos/clerk-webhook.ts | ClerkWebhookCommand / ClerkWebhookResult |
application/ports/user-registration.port.ts | Dependency contract used to register users |
domain/entities/clerk-webhook-event.entity.ts | Domain helpers for event-type decisions and payload extraction |
clerk-fastify-webhooks.d.ts | Type declaration shim for @clerk/fastify/webhooks with current TS module resolution |
Composition dependencies
createClerkWebhooksModule({ registerByIdentityProviderId }) requires:
registerByIdentityProviderId({ identityProviderUserId }): injected from users module use-case
It composes:
handleClerkWebhookuse-case (createHandleClerkWebhookUseCase)- webhook routes (
clerkWebhooksRoutes({ handleClerkWebhook }))
HTTP route
Current endpoint:
POST /webhooks/clerk/v1/user-created
Route adapter behavior:
- registers
fastify-raw-bodyplugin (global: false,runFirst: true) - enables
config: { rawBody: true }on this route - checks that
request.rawBodyexists, otherwise returns:400 { "message": "Missing body" }
- verifies signature with:
verifyWebhook(request, { signingSecret: process.env.CLERK_WEBHOOK_SIGNING_SECRET })
- on verification failure returns:
400 { "message": "Invalid webhook signature" }
- on success calls
handleClerkWebhook({ event })and returns:200 { "received": true, "eventType": "<event.type>" }
Response schemas include:
200inline schema{ received: boolean, eventType: string }400BadRequestResponseSchemafromcontracts/system500InternalServerErrorResponseSchemafromcontracts/system
Use-case and domain behavior
handle-clerk-webhook flow:
- Rehydrate domain entity from Clerk event.
- Check
shouldRegisterUser():- true only for
event.type === "user.created"and non-emptyevent.data.id.
- true only for
- If true, call:
registerByIdentityProviderId({ identityProviderUserId: event.data.id })
- Return normalized result:
{ received: true, eventType: event.type }
Important: verified non-user.created events are still acknowledged with 200, but no registration side-effect is triggered.
Signature and Clerk integration notes
- Signature verification is performed at adapter boundary using
@clerk/fastify/webhooks. - Raw payload is preserved to satisfy Standard Webhooks (Svix) verification requirements.
clerk-fastify-webhooks.d.tsexists because the package subpath typing is not resolved under current TSmoduleResolution: "node"setup.
Runtime flow summary
- Clerk sends webhook to
/webhooks/clerk/v1/user-created. - Route ensures raw body is available.
- Route verifies signature using
CLERK_WEBHOOK_SIGNING_SECRET. - Verified event is passed to
handleClerkWebhook. - For
user.createdwithdata.id, users registration use-case is invoked. - Route returns acknowledgment
{ received: true, eventType }.
Related docs
- Users Module - where
registerByIdentityProviderIdis implemented and composed. - Auth Module - request auth flow for normal user-facing endpoints.
- Contracts and Types - boundaries between DTOs and shared contracts.