Webhooks
All webhook handlers live in src/lib/clerk/. The entry point is src/app/api/webhooks/clerk/route.js, which verifies the Svix signature and dispatches each event to its handler.
Clerk webhooks
user.created
Fires when a new user signs up. Auto-provisions a private test organisation (test-NNNN) so the user lands in a working environment without manual setup.
Source: src/lib/clerk/userCreate.js
The handler runs three steps in sequence:
- Generates a unique slug — picks a
test-NNNNslug not already taken in MongoDB. - Creates the Clerk organisation — calls the Clerk backend API to create the org and set the user as
org:admin. - Creates the MongoDB organisation — calls
organizationFromTemplateCreateto build the org document from the default template and writenewTenantSlugfor the first-login redirect.
If any step fails, the webhook returns a 500 and Clerk retries according to its delivery policy.
Race window: the webhook fires asynchronously, so there is a short gap between user creation and org provisioning. If Clerk's dashboard has Force users to select an organisation enabled, it may show its native create-org UI during this window. Keep that setting OFF in both dev and prod instances, and ensure
afterSignUpUrl="/"is set onClerkProvider.
organization.updated
Handles org metadata changes pushed from Clerk: logo sync, slug rollback (prevents renaming away from the canonical slug), and test-org name/logo lock. Does not do any user reconciliation — membership is always read live from the Clerk API.
Source: src/lib/clerk/organizationUpdate.js
organization.deleted
Removes the MongoDB organisation document when the org is deleted in Clerk.
Source: src/lib/clerk/organizationDelete.js
Architecture notes
User roles and membership are managed entirely by Clerk. MongoDB stores no parallel user, admin, or role data.
getOrgMembership(shortName)insrc/lib/auth/requireOrgAccess.jscalls the Clerk backend API to verify membership at request time.- The
Organizationdocument has nousers,admins, or role fields.
Testing webhooks locally
Clerk cannot deliver webhooks to localhost:3000 without a tunnel.
# Option 1 — Clerk dev proxy (recommended)
npx clerk dev --proxy http://localhost:3000
# Option 2 — ngrok
ngrok http 3000
# then update the webhook endpoint URL in the Clerk dashboard to the ngrok URL
After tunnelling, point the Clerk webhook endpoint at <tunnel-url>/api/webhooks/clerk and subscribe to the relevant events.