# Webhook Testing Guide Complete guide for testing the Clerk webhook integration that syncs users to your database. ## Table of Contents 1. [Prerequisites](#prerequisites) 2. [Local Development Testing](#local-development-testing) 3. [Production Testing](#production-testing) 4. [Test Scenarios](#test-scenarios) 5. [Verification Steps](#verification-steps) 6. [Common Issues](#common-issues) --- ## Prerequisites Before testing webhooks, ensure you have: - ✅ Clerk account and application set up - ✅ Admin app running locally or deployed - ✅ Database schema updated (password field optional) - ✅ `CLERK_WEBHOOK_SECRET` environment variable set - ✅ `svix` package installed in admin app ### Quick Setup Check ```bash # 1. Check environment variables cat apps/admin/.env.local | grep CLERK_WEBHOOK_SECRET # 2. Verify svix is installed cd apps/admin npm list svix # 3. Check database schema cd ../../packages/database npm run db:push ``` --- ## Local Development Testing ### Option 1: Using Clerk CLI (Recommended) The Clerk CLI forwards webhook events from Clerk to your local server. #### Step 1: Install Clerk CLI ```bash npm install -g @clerk/clerk-cli ``` #### Step 2: Login to Clerk ```bash clerk login ``` Follow the prompts to authenticate with your Clerk account. #### Step 3: Start Your Dev Server ```bash cd apps/admin npm run dev ``` Your server should be running at `http://localhost:3000` #### Step 4: Start Webhook Forwarding In a **new terminal window**: ```bash clerk listen --forward-url http://localhost:3000/api/webhooks ``` You'll see output like: ``` ✓ Webhook forwarding is now active! ✓ Webhook secret: whsec_xxxxxxxxxxxxxx Forwarding webhooks to: http://localhost:3000/api/webhooks ``` #### Step 5: Copy the Webhook Secret Copy the webhook secret from the output and add it to your `.env.local`: ```env CLERK_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxx ``` Restart your dev server after adding the secret. #### Step 6: Test with a Real Sign-Up 1. Go to your app (mobile or admin) 2. Sign up a new user 3. Watch both terminals: - Clerk CLI terminal shows the webhook being forwarded - Dev server terminal shows the processing logs Expected output: ``` Received webhook with ID user_abc123 and type user.created ✅ User user_abc123 created in database ``` ### Option 2: Using Webhook Testing Tools #### Using Svix Play (Web UI) 1. Go to [Svix Play](https://www.svix.com/play/) 2. Enter your webhook URL: `http://localhost:3000/api/webhooks` 3. Select "Clerk" as the provider 4. Choose event type: `user.created` 5. Modify the payload if needed 6. Click "Send" **Note:** You'll need to expose your local server (use ngrok) for this to work. #### Using ngrok for Local Testing If you need a public URL: ```bash # Install ngrok npm install -g ngrok # Start ngrok ngrok http 3000 ``` Use the ngrok URL (e.g., `https://abc123.ngrok.io/api/webhooks`) in Clerk Dashboard or Svix Play. --- ## Production Testing ### Step 1: Configure Webhook Endpoint in Clerk Dashboard 1. Go to [Clerk Dashboard](https://dashboard.clerk.com) 2. Select your application 3. Navigate to **Webhooks** in sidebar 4. Click **Add Endpoint** #### Configure Endpoint: - **Endpoint URL:** `https://yourdomain.com/api/webhooks` - **Events to subscribe to:** - ✅ `user.created` - ✅ `user.updated` - ✅ `user.deleted` #### Copy Signing Secret: After creating the endpoint, copy the **Signing Secret** and add it to your production environment variables: ```env CLERK_WEBHOOK_SECRET=whsec_prod_xxxxxxxxxxxxxx ``` ### Step 2: Send Test Event In Clerk Dashboard webhook settings: 1. Click on your endpoint 2. Click **Send Test Event** 3. Select `user.created` 4. Click **Send** ### Step 3: Verify in Dashboard Check the webhook attempt logs: - **Status:** Should be `200 OK` - **Response:** `"Webhook processed successfully"` - **Response Time:** < 2 seconds typically ### Step 4: Verify in Database Query your database to verify the test user was created: ```sql SELECT * FROM users ORDER BY created_at DESC LIMIT 1; ``` --- ## Test Scenarios ### Scenario 1: User Creation **Objective:** Verify new users are synced to database **Steps:** 1. Create a new user via sign-up flow 2. Check database for new user record 3. Verify all fields are populated correctly **Expected Result:** ```sql -- User should exist in database SELECT * FROM users WHERE id = 'user_abc123'; -- Result: -- id: user_abc123 -- email: test@example.com -- firstName: John -- lastName: Doe -- role: client (default) -- password: NULL or empty -- createdAt: [timestamp] -- updatedAt: [timestamp] ``` **Success Criteria:** - ✅ User record exists - ✅ Email matches Clerk - ✅ Name fields populated - ✅ Default role is 'client' - ✅ Timestamps are set ### Scenario 2: User Update **Objective:** Verify user updates sync to database **Steps:** 1. Update user profile in Clerk (name, email, or role) 2. Check database for updated values 3. Verify `updatedAt` timestamp changed **Update Name via Clerk Dashboard:** 1. Go to Users → Select user 2. Update First Name to "Jane" 3. Click Save **Expected Result:** ```sql SELECT * FROM users WHERE id = 'user_abc123'; -- firstName should now be: Jane -- updatedAt should be newer than createdAt ``` **Success Criteria:** - ✅ User fields updated - ✅ `updatedAt` timestamp changed - ✅ `createdAt` timestamp unchanged ### Scenario 3: Role Assignment **Objective:** Verify role metadata syncs correctly **Steps:** 1. Set user role via Clerk Dashboard: - Users → Select user - Public Metadata: `{"role": "trainer"}` - Save 2. Check database for role update **Expected Result:** ```sql SELECT id, email, role FROM users WHERE id = 'user_abc123'; -- role should be: trainer ``` **Success Criteria:** - ✅ Role updated to 'trainer' - ✅ Role persisted in database - ✅ Webhook processed successfully ### Scenario 4: User Deletion **Objective:** Verify user deletion cascades correctly **Steps:** 1. Note user ID: `user_abc123` 2. Delete user from Clerk Dashboard 3. Check database - user should be deleted **Expected Result:** ```sql -- User should not exist SELECT * FROM users WHERE id = 'user_abc123'; -- Returns: 0 rows -- Related records should also be deleted (cascade) SELECT * FROM notifications WHERE user_id = 'user_abc123'; -- Returns: 0 rows ``` **Success Criteria:** - ✅ User record deleted - ✅ Related records deleted (notifications, etc.) - ✅ No orphaned data ### Scenario 5: Bulk User Creation **Objective:** Test webhook handling under load **Steps:** 1. Import multiple users via Clerk Dashboard or API 2. Verify all users created in database 3. Check for any failed webhooks **Expected Result:** ```sql -- All users should exist SELECT COUNT(*) FROM users WHERE created_at > NOW() - INTERVAL 5 MINUTE; -- Should match number of users created ``` **Success Criteria:** - ✅ All users synced - ✅ No duplicate entries - ✅ All webhook attempts successful --- ## Verification Steps ### 1. Check Webhook Logs (Clerk Dashboard) **Location:** Clerk Dashboard → Webhooks → Your Endpoint **What to Check:** - **Status Code:** Should be `200` - **Response Time:** Typically < 2 seconds - **Response Body:** `"Webhook processed successfully"` - **Attempts:** Should be 1 (no retries needed) **Red Flags:** - ❌ Status 400: Verification failed (wrong secret) - ❌ Status 500: Server error (check logs) - ❌ Multiple attempts: Server issues or slow response ### 2. Check Server Logs **Look for these log messages:** ```bash # Success logs ✅ User user_abc123 created in database ✅ User user_abc123 updated in database ✅ User user_abc123 deleted from database # Info logs Received webhook with ID user_abc123 and type user.created # Error logs (should not appear) ❌ Error verifying webhook: [details] ❌ Error processing webhook: [details] ❌ No primary email found for user: [id] ``` ### 3. Verify Database State **Query Templates:** ```sql -- Check recent users SELECT * FROM users ORDER BY created_at DESC LIMIT 10; -- Check user count by role SELECT role, COUNT(*) as count FROM users GROUP BY role; -- Check for users without required fields SELECT * FROM users WHERE email IS NULL OR firstName = '' OR lastName = ''; -- Check for orphaned records (shouldn't exist after cascade delete) SELECT n.* FROM notifications n LEFT JOIN users u ON n.user_id = u.id WHERE u.id IS NULL; ``` ### 4. Test Role Assignment API **Using curl:** ```bash # Get your auth token from Clerk TOKEN="your_clerk_session_token" # Set user role curl -X POST http://localhost:3000/api/admin/set-role \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{ "targetUserId": "user_abc123", "role": "trainer" }' # Expected response: # { # "success": true, # "message": "User role updated to trainer", # "user": { ... } # } ``` --- ## Common Issues ### Issue 1: Webhook Returns 400 - Verification Failed **Symptoms:** ``` Error verifying webhook: [Svix error details] Status: 400 ``` **Causes:** - Wrong webhook secret - Missing Svix headers - Replay attack (old timestamp) **Solutions:** 1. **Verify webhook secret:** ```bash echo $CLERK_WEBHOOK_SECRET # Should start with whsec_ ``` 2. **Regenerate secret:** - Go to Clerk Dashboard → Webhooks - Click your endpoint → Rotate Secret - Update `.env.local` with new secret - Restart server 3. **Check headers:** ```typescript // Add debug logging to webhook handler console.log('Headers:', { svix_id: headerPayload.get('svix-id'), svix_timestamp: headerPayload.get('svix-timestamp'), svix_signature: headerPayload.get('svix-signature'), }); ``` ### Issue 2: User Not Created in Database **Symptoms:** - Webhook returns 200 - Server logs show success - User not in database **Solutions:** 1. **Check database connection:** ```bash cd packages/database npm run db:studio # Verify you can see the database ``` 2. **Check for unique constraint violations:** ```sql -- Check if user with email already exists SELECT * FROM users WHERE email = 'test@example.com'; ``` 3. **Verify schema migration ran:** ```bash cd packages/database npm run db:push ``` 4. **Check server logs for SQL errors:** ``` Error processing webhook: UNIQUE constraint failed: users.email ``` ### Issue 3: Role Not Updating **Symptoms:** - Webhook processed successfully - Role in database doesn't change **Solutions:** 1. **Verify metadata format in Clerk:** ```json { "role": "admin" } ``` (Should be in **Public Metadata**, not Private or Unsafe) 2. **Check role value:** - Must be exactly: `admin`, `trainer`, or `client` - Case-sensitive - No extra spaces 3. **Force webhook retry:** - Update any field on the user - This triggers `user.updated` webhook ### Issue 4: Webhook Not Received **Symptoms:** - Event in Clerk - No webhook attempt logged - Server never hit **Solutions:** 1. **Verify endpoint URL in Clerk Dashboard:** ``` Production: https://yourdomain.com/api/webhooks Local (with ngrok): https://abc123.ngrok.io/api/webhooks ``` 2. **Check subscribed events:** - Ensure `user.created`, `user.updated`, `user.deleted` are checked 3. **Verify server is accessible:** ```bash # Test endpoint manually curl -X POST https://yourdomain.com/api/webhooks \ -H "Content-Type: application/json" \ -d '{"test": true}' # Should return 400 (missing headers), not 404 ``` 4. **Check firewall/security rules:** - Ensure Clerk IPs not blocked - HTTPS must be valid (not self-signed cert) ### Issue 5: Slow Webhook Processing **Symptoms:** - Webhook takes > 5 seconds - Clerk retries multiple times - Timeouts **Solutions:** 1. **Optimize database queries:** ```typescript // Add indexes // Run migrations separately // Use transactions for bulk operations ``` 2. **Add timeout handling:** ```typescript // Process webhook asynchronously // Return 200 immediately // Handle sync in background ``` 3. **Check database connection pool:** ```typescript // Ensure connection pool is sized correctly // Close connections properly ``` --- ## Automated Testing ### Unit Test Example ```typescript // apps/admin/__tests__/webhook.test.ts import { POST } from '@/app/api/webhooks/route'; describe('Webhook Handler', () => { it('should create user on user.created event', async () => { const mockRequest = new Request('http://localhost/api/webhooks', { method: 'POST', headers: { 'svix-id': 'msg_test', 'svix-timestamp': Date.now().toString(), 'svix-signature': 'test_signature', }, body: JSON.stringify({ type: 'user.created', data: { id: 'user_test123', email_addresses: [ { id: 'email_test', email_address: 'test@example.com', }, ], primary_email_address_id: 'email_test', first_name: 'Test', last_name: 'User', public_metadata: { role: 'client' }, }, }), }); const response = await POST(mockRequest); expect(response.status).toBe(200); }); }); ``` --- ## Monitoring Checklist Use this checklist for ongoing webhook monitoring: - [ ] Check webhook success rate in Clerk Dashboard (should be > 99%) - [ ] Monitor webhook response times (should be < 2s) - [ ] Review failed webhook attempts weekly - [ ] Verify user count in Clerk matches database - [ ] Check for orphaned records monthly - [ ] Test role assignment flow quarterly - [ ] Review webhook logs for errors - [ ] Validate cascade deletes are working --- ## Next Steps After Testing Once webhooks are tested and working: 1. ✅ **Document for your team** - Share this guide 2. ✅ **Set up monitoring** - Use Clerk Dashboard or custom monitoring 3. ✅ **Plan for scale** - Consider async processing for high volume 4. ✅ **Implement role UI** - Build admin interface for role management 5. ✅ **Add audit logging** - Track who changes what roles 6. ✅ **Test failure scenarios** - What happens if database is down? --- ## Support Resources - **Clerk Webhooks Docs:** https://clerk.com/docs/integrations/webhooks - **Svix Documentation:** https://docs.svix.com - **Project Documentation:** See `CLERK_WEBHOOK_SETUP.md` - **Troubleshooting:** See `TROUBLESHOOTING.md` --- **Last Updated:** 2024 **Webhook Handler:** `apps/admin/src/app/api/webhooks/route.ts`