import { Webhook } from 'svix'
import { headers } from 'next/headers'
import type { WebhookEvent } from '@clerk/nextjs/server'
export async function POST(req: Request) {
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET
if (!WEBHOOK_SECRET) {
throw new Error('Missing CLERK_WEBHOOK_SECRET env variable')
}
const headerPayload = await headers()
const svixHeaders = {
'svix-id': headerPayload.get('svix-id') || '',
'svix-timestamp': headerPayload.get('svix-timestamp') || '',
'svix-signature': headerPayload.get('svix-signature') || '',
}
if (!svixHeaders['svix-id'] || !svixHeaders['svix-signature']) {
return new Response('Missing svix headers', { status: 400 })
}
const body = await req.text()
const wh = new Webhook(WEBHOOK_SECRET)
let evt: WebhookEvent
try {
evt = wh.verify(body, svixHeaders) as WebhookEvent
} catch (err) {
console.error('Webhook verification failed:', err)
return new Response('Invalid signature', { status: 400 })
}
return handleWebhookEvent(evt)
}
async function handleWebhookEvent(evt: WebhookEvent) {
const eventType = evt.type
switch (eventType) {
case 'user.created': {
const { id, email_addresses, first_name, last_name, image_url } = evt.data
const primaryEmail = email_addresses.find(e => e.id === evt.data.primary_email_address_id)
await db.user.create({
data: {
clerkId: id,
email: primaryEmail?.email_address || email_addresses[0]?.email_address,
firstName: first_name,
lastName: last_name,
avatarUrl: image_url,
},
})
console.log(`[Webhook] User created: ${id}`)
break
}
case 'user.updated': {
const { id, email_addresses, first_name, last_name, image_url } = evt.data
const primaryEmail = email_addresses.find(e => e.id === evt.data.primary_email_address_id)
await db.user.upsert({
where: { clerkId: id },
update: {
email: primaryEmail?.email_address,
firstName: first_name,
lastName: last_name,
avatarUrl: image_url,
},
create: {
clerkId: id,
email: primaryEmail?.email_address || '',
firstName: first_name,
lastName: last_name,
avatarUrl: image_url,
},
})
break
}
case 'user.deleted': {
if (evt.data.id) {
await db.user.update({
where: { clerkId: evt.data.id },
data: { deletedAt: new Date() },
})
}
break
}
case 'organization.created': {
const { id, name, slug, created_by } = evt.data
await db.organization.create({
data: { clerkOrgId: id, name, slug: slug || '', createdBy: created_by },
})
break
}
case 'organizationMembership.created': {
const { organization, public_user_data, role } = evt.data
await db.orgMembership.create({
data: {
orgId: organization.id,
userId: public_user_data.user_id,
role,
},
})
break
}
case 'session.created':
console.log(`[Webhook] Session created for user: ${evt.data.user_id}`)
break
default:
console.log(`[Webhook] Unhandled event: ${eventType}`)
}
return new Response('OK', { status: 200 })
}
export async function processIdempotently(
svixId: string,
eventType: string,
handler: () => Promise<void>
): Promise<{ processed: boolean; duplicate: boolean }> {
const existing = await db.webhookEvent.findUnique({
where: { svixId },
})
if (existing) {
console.log(`[Webhook] Duplicate event skipped: ${svixId} (${eventType})`)
return { processed: false, duplicate: true }
}
await db.webhookEvent.create({
data: { svixId, eventType, status: 'processing', receivedAt: new Date() },
})
try {
await handler()
await db.webhookEvent.update({
where: { svixId },
data: { status: 'completed', processedAt: new Date() },
})
return { processed: true, duplicate: false }
} catch (error) {
await db.webhookEvent.update({
where: { svixId },
data: { status: 'failed', error: String(error) },
})
throw error
}
}
import express from 'express'
import { Webhook } from 'svix'
const app = express()
app.post('/api/webhooks/clerk',
express.raw({ type: 'application/json' }),
(req, res) => {
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!)
try {
const evt = wh.verify(req.body, {
'svix-id': req.headers['svix-id'] as string,
'svix-timestamp': req.headers['svix-timestamp'] as string,
'svix-signature': req.headers['svix-signature'] as string,
})
res.status(200).json({ received: true })
} catch (err) {
console.error('Webhook verification failed:', err)
res.status(400).json({ error: 'Invalid signature' })
}
}
)