Skip to main content

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:

  1. Generates a unique slug — picks a test-NNNN slug not already taken in MongoDB.
  2. Creates the Clerk organisation — calls the Clerk backend API to create the org and set the user as org:admin.
  3. Creates the MongoDB organisation — calls organizationFromTemplateCreate to build the org document from the default template and write newTenantSlug for 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 on ClerkProvider.


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) in src/lib/auth/requireOrgAccess.js calls the Clerk backend API to verify membership at request time.
  • The Organization document has no users, 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.