# Clerk Webhook Setup Guide This guide explains how to set up Clerk webhooks to automatically sync user data between Clerk and your local database. ## Overview When users sign up, update their profile, or delete their account in Clerk, webhooks automatically sync these changes to your local database. This ensures your database always has up-to-date user information without manual intervention. ## Architecture ``` Clerk Event (user.created/updated/deleted) ↓ Clerk Webhook → Your API Endpoint (/api/webhooks) ↓ Webhook Handler (verifies signature) ↓ Database Sync (insert/update/delete user) ``` ## What Gets Synced The webhook handler syncs the following user data: - **User ID** - Clerk user ID (primary key) - **Email** - Primary email address - **First Name** - User's first name - **Last Name** - User's last name - **Role** - From `public_metadata.role` (defaults to 'client') - **Phone** - Optional phone number - **Timestamps** - Created/updated timestamps ## Setup Instructions ### 1. Get Your Webhook Secret #### Development (Local Testing) 1. Install Clerk CLI: ```bash npm install -g @clerk/clerk-cli ``` 2. Login to Clerk CLI: ```bash clerk login ``` 3. Start webhook forwarding (in a new terminal): ```bash clerk listen --forward-url http://localhost:3000/api/webhooks ``` This will output a webhook secret like: `whsec_xxxxxxxxxxxxx` #### Production 1. Go to [Clerk Dashboard](https://dashboard.clerk.com) 2. Select your application 3. Navigate to **Webhooks** in the left sidebar 4. Click **Add Endpoint** 5. Enter your production URL: `https://yourdomain.com/api/webhooks` 6. Select the events to subscribe to: - `user.created` - `user.updated` - `user.deleted` 7. Copy the **Signing Secret** ### 2. Add Environment Variables Add the webhook secret to your environment variables: **For Admin App (`apps/admin/.env.local`):** ```env # Clerk Configuration NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxx CLERK_SECRET_KEY=sk_test_xxxxxxxxxxxxx # Webhook Secret (from step 1) CLERK_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxx ``` ### 3. Update Database Schema The database schema has been updated to make the `password` field optional since Clerk handles authentication: ```typescript export const users = sqliteTable("users", { id: text("id").primaryKey(), email: text("email").notNull().unique(), firstName: text("first_name").notNull(), lastName: text("last_name").notNull(), password: text("password"), // Optional - Clerk handles auth role: text("role", { enum: ["admin", "trainer", "client"] }) .notNull() .default("client"), phone: text("phone"), createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .$defaultFn(() => new Date()), updatedAt: integer("updated_at", { mode: "timestamp" }) .notNull() .$defaultFn(() => new Date()), }); ``` ### 4. Run Database Migration Apply the schema changes to your database: ```bash cd packages/database npm run db:push ``` ### 5. Test the Webhook #### Using Clerk CLI (Development) 1. Start your Next.js dev server: ```bash cd apps/admin npm run dev ``` 2. In another terminal, start Clerk webhook forwarding: ```bash clerk listen --forward-url http://localhost:3000/api/webhooks ``` 3. Create a test user in Clerk Dashboard or through your app 4. Check your terminal logs - you should see: ``` ✅ User user_xxxxx created in database ``` #### Using Clerk Dashboard (Production) 1. Go to your webhook endpoint in Clerk Dashboard 2. Click **Send Test Event** 3. Select `user.created` event type 4. Click **Send** 5. Check the webhook logs for success (200 status) ## Webhook Events Handled ### `user.created` Triggered when a new user signs up. **Action:** Inserts a new user record into the database. **Payload Example:** ```json { "type": "user.created", "data": { "id": "user_2abc123", "email_addresses": [ { "id": "idn_xyz789", "email_address": "user@example.com" } ], "primary_email_address_id": "idn_xyz789", "first_name": "John", "last_name": "Doe", "public_metadata": { "role": "client" } } } ``` ### `user.updated` Triggered when a user updates their profile. **Action:** Updates the user record in the database. **Fields Updated:** - Email (if changed) - First name - Last name - Role (from public_metadata) - Updated timestamp ### `user.deleted` Triggered when a user is deleted from Clerk. **Action:** Deletes the user from the database (cascades to related records). ## Setting User Roles To assign roles to users, update their `public_metadata` in Clerk: ### Option 1: Clerk Dashboard 1. Go to **Users** in Clerk Dashboard 2. Select a user 3. Scroll to **Public Metadata** 4. Add: ```json { "role": "admin" } ``` 5. Click **Save** ### Option 2: Programmatically (Backend) ```typescript import { clerkClient } from '@clerk/nextjs/server'; async function setUserRole(userId: string, role: 'admin' | 'trainer' | 'client') { await clerkClient.users.updateUser(userId, { publicMetadata: { role }, }); } ``` ### Option 3: On Sign-Up (Custom Flow) ```typescript // In your sign-up flow await signUp.create({ emailAddress, password, firstName, lastName, unsafeMetadata: { // Store temporarily during sign-up role: 'client', }, }); // After email verification, move to public_metadata await user.update({ publicMetadata: { role: user.unsafeMetadata.role, }, }); ``` ## Security ### Webhook Signature Verification The webhook handler **always verifies** the signature of incoming requests using Svix: ```typescript const wh = new Webhook(WEBHOOK_SECRET); const evt = wh.verify(body, { 'svix-id': svix_id, 'svix-timestamp': svix_timestamp, 'svix-signature': svix_signature, }); ``` This ensures: - ✅ Requests come from Clerk (not malicious actors) - ✅ Payload hasn't been tampered with - ✅ Request is recent (prevents replay attacks) ### Best Practices 1. **Keep your webhook secret secure** - Never commit it to version control 2. **Use HTTPS in production** - Webhooks should only be sent over HTTPS 3. **Monitor webhook logs** - Check for failed webhooks regularly 4. **Handle errors gracefully** - The handler returns appropriate status codes 5. **Test thoroughly** - Use Clerk CLI for local testing before deploying ## Troubleshooting ### Webhook Returns 400 Error **Problem:** Missing Svix headers or invalid signature **Solutions:** - Verify `CLERK_WEBHOOK_SECRET` is set correctly - Check that you're using the correct webhook secret for your environment - Ensure the endpoint URL in Clerk Dashboard is correct ### User Not Created in Database **Problem:** Webhook received but user not inserted **Solutions:** - Check server logs for errors - Verify database connection is working - Ensure user doesn't already exist (duplicate email) - Check that schema migration was applied ### Webhook Not Received **Problem:** Events in Clerk but no webhook calls **Solutions:** - Verify webhook endpoint is configured in Clerk Dashboard - Check that events are subscribed (`user.created`, `user.updated`, `user.deleted`) - Ensure your server is publicly accessible (for production) - For local dev, ensure `clerk listen` is running ### Role Not Set Correctly **Problem:** User created but role is always 'client' **Solutions:** - Check that `public_metadata.role` is set in Clerk - Verify the role value is one of: `admin`, `trainer`, `client` - Use `unsafeMetadata` during sign-up, then move to `publicMetadata` after verification ## Monitoring ### View Webhook Logs in Clerk Dashboard 1. Go to **Webhooks** in Clerk Dashboard 2. Click on your endpoint 3. View recent webhook attempts, status codes, and payloads 4. Use **Retry** button for failed webhooks ### Check Your Server Logs The webhook handler logs important events: ``` ✅ User user_abc123 created in database ✅ User user_abc123 updated in database ✅ User user_abc123 deleted from database ``` Errors are also logged: ``` Error verifying webhook: [details] Error processing webhook: [details] ``` ## Migration Strategy If you have existing users in your database (from before Clerk integration): ### Option 1: Import to Clerk Use Clerk's user import feature or API to bulk import existing users. ### Option 2: Dual Authentication Keep both auth systems temporarily and gradually migrate users: ```typescript // Check if user exists in Clerk first const clerkUser = await clerkClient.users.getUser(userId); // If not, check local database if (!clerkUser) { const localUser = await db.query.users.findFirst({ where: eq(users.email, email), }); if (localUser && localUser.password) { // Migrate to Clerk await clerkClient.users.createUser({ emailAddress: [localUser.email], firstName: localUser.firstName, lastName: localUser.lastName, publicMetadata: { role: localUser.role }, }); } } ``` ## Next Steps After setting up webhooks: 1. ✅ **Test user creation** - Sign up a new user and verify database sync 2. ✅ **Test user updates** - Update profile and verify sync 3. ✅ **Set up roles** - Assign admin/trainer roles to appropriate users 4. ✅ **Monitor webhooks** - Check Clerk Dashboard for webhook health 5. ✅ **Document for team** - Share this guide with your team ## Related Documentation - [Clerk Webhooks Documentation](https://clerk.com/docs/integrations/webhooks/overview) - [Svix Webhook Verification](https://docs.svix.com/receiving/verifying-payloads/how) - [Next.js API Routes](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) - [Drizzle ORM](https://orm.drizzle.team/docs/overview) ## Support If you encounter issues: 1. Check this guide's troubleshooting section 2. Review Clerk webhook logs in the dashboard 3. Check your server logs for errors 4. Consult the main `TROUBLESHOOTING.md` file 5. Contact Clerk support or open a GitHub issue --- **Webhook Handler Location:** `apps/admin/src/app/api/webhooks/route.ts` **Last Updated:** 2024