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)
-
Install Clerk CLI:
npm install -g @clerk/clerk-cli -
Login to Clerk CLI:
clerk login -
Start webhook forwarding (in a new terminal):
clerk listen --forward-url http://localhost:3000/api/webhooksThis will output a webhook secret like:
whsec_xxxxxxxxxxxxx
Production
- Go to Clerk Dashboard
- Select your application
- Navigate to Webhooks in the left sidebar
- Click Add Endpoint
- Enter your production URL:
https://yourdomain.com/api/webhooks - Select the events to subscribe to:
user.createduser.updateduser.deleted
- 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)
-
Start your Next.js dev server:
cd apps/admin npm run dev -
In another terminal, start Clerk webhook forwarding:
clerk listen --forward-url http://localhost:3000/api/webhooks -
Create a test user in Clerk Dashboard or through your app
-
Check your terminal logs - you should see:
✅ User user_xxxxx created in database
Using Clerk Dashboard (Production)
- Go to your webhook endpoint in Clerk Dashboard
- Click Send Test Event
- Select
user.createdevent type - Click Send
- 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
- Go to Users in Clerk Dashboard
- Select a user
- Scroll to Public Metadata
- Add:
{ "role": "admin" } - 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
- Keep your webhook secret secure - Never commit it to version control
- Use HTTPS in production - Webhooks should only be sent over HTTPS
- Monitor webhook logs - Check for failed webhooks regularly
- Handle errors gracefully - The handler returns appropriate status codes
- 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_SECRETis 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 listenis running
Role Not Set Correctly
Problem: User created but role is always 'client'
Solutions:
- Check that
public_metadata.roleis set in Clerk - Verify the role value is one of:
admin,trainer,client - Use
unsafeMetadataduring sign-up, then move topublicMetadataafter verification
Monitoring
View Webhook Logs in Clerk Dashboard
- Go to Webhooks in Clerk Dashboard
- Click on your endpoint
- View recent webhook attempts, status codes, and payloads
- 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:
- ✅ Test user creation - Sign up a new user and verify database sync
- ✅ Test user updates - Update profile and verify sync
- ✅ Set up roles - Assign admin/trainer roles to appropriate users
- ✅ Monitor webhooks - Check Clerk Dashboard for webhook health
- ✅ Document for team - Share this guide with your team
Related Documentation
Support
If you encounter issues:
- Check this guide's troubleshooting section
- Review Clerk webhook logs in the dashboard
- Check your server logs for errors
- Consult the main
TROUBLESHOOTING.mdfile - Contact Clerk support or open a GitHub issue
Webhook Handler Location: apps/admin/src/app/api/webhooks/route.ts
Last Updated: 2024