406 lines
9.9 KiB
Markdown
406 lines
9.9 KiB
Markdown
# 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 |