fitaiProto/docs/CLERK_WEBHOOK_SETUP.md
2025-11-19 05:12:19 +01:00

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