fitaiProto/CLERK_WEBHOOK_SETUP.md
echo 3a58d420d6 clerkauth
implemented, sync with db to be added
2025-11-10 04:16:31 +01:00

9.9 KiB

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:

    npm install -g @clerk/clerk-cli
    
  2. Login to Clerk CLI:

    clerk login
    
  3. Start webhook forwarding (in a new terminal):

    clerk listen --forward-url http://localhost:3000/api/webhooks
    

    This will output a webhook secret like: whsec_xxxxxxxxxxxxx

Production

  1. Go to Clerk Dashboard
  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):

# 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:

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:

cd packages/database
npm run db:push

5. Test the Webhook

Using Clerk CLI (Development)

  1. Start your Next.js dev server:

    cd apps/admin
    npm run dev
    
  2. In another terminal, start Clerk webhook forwarding:

    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:

{
  "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:
    {
      "role": "admin"
    }
    
  5. Click Save

Option 2: Programmatically (Backend)

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)

// 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:

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:

// 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

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