Clerk Webhooks with Prisma (Sync database, user model)

Hi, does anyone have a working example of how to sync user model via Prisma with Clerk? Clerk recommends using webhooks but the process has proven to be very tedious with little in the way of documentation. I know you can instead be sneaky and use the afterAuth middleware instead, so I'll take those examples as well.
25 Replies
jaronheard
jaronheard12mo ago
This is what I'm doing https://github.com/jaronheard/timetime.cc/blob/main/app/api/webhooks/clerk/route.ts Pretty much just followed their docs
GitHub
timetime.cc/app/api/webhooks/clerk/route.ts at main · jaronheard/ti...
Curate calendars, cultivate communities. Contribute to jaronheard/timetime.cc development by creating an account on GitHub.
Circus
CircusOP12mo ago
What did you do to make it work in production? I followed these documents too and the processes just get held up for eternity
No description
NotLuksus
NotLuksus12mo ago
Does this error? If you click in on th event it show some kind of reason / response from your server? Is it deployed to vercel?
Circus
CircusOP12mo ago
Yeah it gives a 308 on (some) of the logs that fail. And then they restart or something? Because the original attempts or still attempting 24 hours later, just with the failure rate increasing
Sydney 🔪
Sydney 🔪12mo ago
@Circus This page of the documentation basically gives you all the snippets you need to set it up https://clerk.com/docs/users/sync-data That’ll get the data to your api, then you just have to pass it to the relevant db function ☺️
Sync Clerk data to your backend with webhooks | Clerk
The recommended way to sync data between Clerk and your application's backend is via webhooks. In this guide you'll learn how to enable webhooks and how to set up your backend so that it is updated every time an event happens on your Clerk instance.
Circus
CircusOP12mo ago
@Sydney Thanks for your reply. Unfortunately, this isn't the case (I've scoured this page many times as one can imagine). The webhook works with ngrok and its respective keys, but the communication with Clerk in production is causing the eternal "attempting message."
Sydney 🔪
Sydney 🔪12mo ago
Strange, Im basically using that setup to the letter and just passing it on to my drizzle function and it’s working like a charm I’ll compare what mine looks like when I have a moment and let you know if there’s anything extra i did
Circus
CircusOP12mo ago
Yeah, I'd love to see webhooks actually work as expected. I'm pretty close to just scrapping using (Clerk) webhooks at all as they've been nothing but a headache and just handling what I want through alternative + less efficient means
Sydney 🔪
Sydney 🔪12mo ago
export async function POST(req: Request) {
// You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET!;

if (!WEBHOOK_SECRET) {
throw new Error(
"Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local",
);
}

// Get the headers
const headerPayload = headers();
const svix_id = headerPayload.get("svix-id");
const svix_timestamp = headerPayload.get("svix-timestamp");
const svix_signature = headerPayload.get("svix-signature");

// If there are no headers, error out
if (!svix_id || !svix_timestamp || !svix_signature) {
return new Response("Error occured -- no svix headers", {
status: 400,
});
}

// Get the body
const payload = (await req.json()) as Request;
const body = JSON.stringify(payload);

// Create a new SVIX instance with your secret.
const wh = new Webhook(WEBHOOK_SECRET);

let evt: WebhookEvent;

// Verify the payload with the headers
try {
evt = wh.verify(body, {
"svix-id": svix_id,
"svix-timestamp": svix_timestamp,
"svix-signature": svix_signature,
}) as WebhookEvent;
} catch (err) {
console.error("Error verifying webhook:", err);
return new Response("Error occured", {
status: 400,
});
}

const { id, username, image_url } = evt.data as UserJSON;

const eventType = evt.type;
// Manage user events
if (eventType === "user.created") {
await initiateUserProfile({
userId: id,
username: username,
displayPicture: image_url,
}).then(() => {
return new Response("", { status: 201 });
}),
(err: unknown) => {
console.error("Error creating user profile:", err);
return new Response("Error occured", {
status: 400,
});
};
}

return new Response("boop", { status: 201 });
}
export async function POST(req: Request) {
// You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET!;

if (!WEBHOOK_SECRET) {
throw new Error(
"Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local",
);
}

// Get the headers
const headerPayload = headers();
const svix_id = headerPayload.get("svix-id");
const svix_timestamp = headerPayload.get("svix-timestamp");
const svix_signature = headerPayload.get("svix-signature");

// If there are no headers, error out
if (!svix_id || !svix_timestamp || !svix_signature) {
return new Response("Error occured -- no svix headers", {
status: 400,
});
}

// Get the body
const payload = (await req.json()) as Request;
const body = JSON.stringify(payload);

// Create a new SVIX instance with your secret.
const wh = new Webhook(WEBHOOK_SECRET);

let evt: WebhookEvent;

// Verify the payload with the headers
try {
evt = wh.verify(body, {
"svix-id": svix_id,
"svix-timestamp": svix_timestamp,
"svix-signature": svix_signature,
}) as WebhookEvent;
} catch (err) {
console.error("Error verifying webhook:", err);
return new Response("Error occured", {
status: 400,
});
}

const { id, username, image_url } = evt.data as UserJSON;

const eventType = evt.type;
// Manage user events
if (eventType === "user.created") {
await initiateUserProfile({
userId: id,
username: username,
displayPicture: image_url,
}).then(() => {
return new Response("", { status: 201 });
}),
(err: unknown) => {
console.error("Error creating user profile:", err);
return new Response("Error occured", {
status: 400,
});
};
}

return new Response("boop", { status: 201 });
}
若帆 Leo
若帆 Leo12mo ago
I just don’t sync with the db, just use the useId, unless you really need that relation?
Sydney 🔪
Sydney 🔪12mo ago
was not joking about it being to the letter, still have the comments and all ahahah
Circus
CircusOP12mo ago
Yeah, mine looks very identical to yours as well haha, and it does work locally/ngrok. But once it's in Clerk Webhookland in production, it goes into limbo forever
Sydney 🔪
Sydney 🔪12mo ago
Have you had a look at the vercel logs for the api request to see what they’re saying? Also might be stupid but if you set up a custom domain… is the www. The primary with the base url redirecting to it? In the past I’ve forgotten that the www was the primary when trying to point at the api and it would keep bouncing lmao
NotLuksus
NotLuksus12mo ago
You need to make the webhook url the www. version of your domain since when you set up a domain on vercel with the default settings it will by default redirect you there However the way this webhooks work, they treat 308 as errors and dont actually follow that redirect
Circus
CircusOP12mo ago
Well, that's...something haha That was the issue - thank you @Sydney and @NotLuksus !
Sydney 🔪
Sydney 🔪12mo ago
I’m sorry that was the solution but glad we could help ahahah
divox
divox12mo ago
Hey @Circus, I’m currently trying to sync clerk with my db. Quick question, are you able to trigger your webhook when updating username inside of the user button?
Circus
CircusOP12mo ago
Hey @divox, right now I'm not triggering anything other than the "user.created" webhook event. I'll eventually want to have a similar event to what you're atempting, so I think what you and I will both be utilizing is the "user.updated" event (https://clerk.com/docs/integrations/webhooks). I wish the Clerk documentation was more thorough in these instances too
divox
divox12mo ago
Gotcha. Yea I spent some time yesterday trying to get the user.updated to trigger when I used the “update username” feature within the user button, but haven’t had any success yet. I’ll follow up in here if I get it working 👍
Circus
CircusOP12mo ago
I'm assuming you have custom login (i.e. users can create username and password for your site)? I only have login through Faceobok, Google, Github, etc. and I don't have the option to change username. I will eventually want the webhook to fire when the user changes their profile image on one of their accounts though
Sydney 🔪
Sydney 🔪12mo ago
Kinda strange that it’s not working for you as updating it in the user button still updates the user details in clerk… it getting updated there should still trigger the webhook the same as updating the details in any other way would, weird
vibbin
vibbin12mo ago
I'm currently doing this without webhooks but feel like I should be using webhooks. Currently after signin/signup I am redirecting to /new-user
import { db } from '@/server/db/client'
import { currentUser } from '@clerk/nextjs'
import { redirect } from 'next/navigation'

const createNewUser = async (): Promise<void> => {
try {
const clerkUser = await currentUser()

if (!clerkUser?.id) {
throw new Error('Clerk user ID is undefined.')
}

const match = await db.user.findUnique({
where: {
clerkId: clerkUser.id,
},
})

if (match && match.onboarded) {
redirect('/')
} else if (!match) {
await db.user.create({
data: {
clerkId: clerkUser.id,
name: clerkUser.firstName,
email: clerkUser.emailAddresses[0]?.emailAddress,
createdAt: new Date(),

},
})
redirect('/onboarding')
}

redirect('/onboarding')
} catch (error) {
console.error('Error creating new user:', error)
}
}

const NewUser = async () => {
await createNewUser()
return <div>...loading</div>
}

export default NewUser
import { db } from '@/server/db/client'
import { currentUser } from '@clerk/nextjs'
import { redirect } from 'next/navigation'

const createNewUser = async (): Promise<void> => {
try {
const clerkUser = await currentUser()

if (!clerkUser?.id) {
throw new Error('Clerk user ID is undefined.')
}

const match = await db.user.findUnique({
where: {
clerkId: clerkUser.id,
},
})

if (match && match.onboarded) {
redirect('/')
} else if (!match) {
await db.user.create({
data: {
clerkId: clerkUser.id,
name: clerkUser.firstName,
email: clerkUser.emailAddresses[0]?.emailAddress,
createdAt: new Date(),

},
})
redirect('/onboarding')
}

redirect('/onboarding')
} catch (error) {
console.error('Error creating new user:', error)
}
}

const NewUser = async () => {
await createNewUser()
return <div>...loading</div>
}

export default NewUser
I have my own User model that has an onboarding field of boolean. If I integrated webhooks how fast is this? After signup and redirect to /onboarding will it create a new user in my db in time for the user? How do people handle this in Clerk when you need to onboard users with your own data?
NotLuksus
NotLuksus12mo ago
I do an upsert in the webhook and at the end of my onboarding The way it works for me, create the clerk user with their email, I use oauth and magic email codes, and the final screen of the onboarding is where the user inputs there information. At that point when I use auth() / currentUser() I have the 'basic user' already and can use its id as my user id when i save the final data to the db
Eternity
Eternity11mo ago
Is there a way for the webhook to redirect to dashboard but only after the user is created?
NotLuksus
NotLuksus11mo ago
The webhook isn't called by a user but from clerks servers, any redirects should happen for the client that makes these calls
Want results from more Discord servers?
Add your server