Handling Clerk webhook events
Third party authentication providers like Clerk are a fantastic way to add auth, user management, and security features to your application. They also provide drop-in components that can get your auth set up quickly. However, with an external source of truth for auth, you'll often need to:
- Sync data from Clerk with your database,
- Provision resources for new accounts, or
- Trigger other work from events (such as emails).
This page offers a guide on setting up a Clerk webhook with Inngest and using Clerk events within Inngest functions.
Setting up the Clerk webhook
Clerk enables sending events to a webhook endpoint when certain events occur. Inngest's webhook endpoints allow you to receive these events within your account just like events that you send from your own application.
To set up the Clerk webhook, open the Clerk dashboard and navigate to the "Webhooks" page. Next, select the "Add Endpoint" button.
On the next page, select the "Transformation" template tab and the Inngest template, then click on the "Connect to Inngest" button.
A popup window will appear to complete the setup. Select "Approve" to create the webhook.
After the popup window disappears, the Webhooks page will now display "Connected" with the webhook URL underneath. There is one more step to complete setup.
To complete the setup, scroll down and select "Create".
You'll be redirected to the new endpoint. In your Inngest dashboard, you will see a new webhook created in your account's production environment.
Creating a function to sync a new user to a database
Often, one key part of integrating with an auth provider like Clerk is handling asynchronous updates with a webhook.
Suppose you need to write a function which will insert a new user into the database which will be triggered whenever clerk/user.created
event occurs. You would use the inngest.createFunction()
method, like in the example below:
src/inngest/sync-user.ts
const syncUser = inngest.createFunction(
{ id: 'sync-user-from-clerk' },
{ event: 'clerk/user.created' },
async ({ event }) => {
// The event payload's data will be the Clerk User json object
const { user } = event.data;
const { id, first_name, last_name } = user;
const email = user.email_addresses.find(e =>
e.id === user.primary_email_address_id
).email;
await database.users.insert({ id, email, first_name, last_name });
}
)
The event
object contains all of the relevant data for the event. The event.data
will match the data
object from the standard Clerk webhook payload structure. With this clerk/user.created
event, the event.data
will be a Clerk User json object.
As you can see, you can choose which events you want to handle with each function. You might write a separate function for clerk/user.updated
and clerk/user.deleted
handling the entire lifecycle end to end.
Note that multiple functions can also listen to the same event. This pattern is called “fan-out.”
Creating a function to send a welcome email
Often, applications need to perform additional tasks when a new user is created, like send a welcome email with tips and useful information.
While it is possible to add this logic at the end of your sync function as seen in the previous section, it’s better to decouple unrelated tasks into different functions so issues with one task do not affect the other ones. For example, if your email fails to send, it should not affect starting a trial for that user in Stripe.
You can make use of the fact that with Inngest, each function has automatic retries, so only the code that has issues is re-run.
The code below creates another function using the same clerk/user.created
event and adds the logic to send the welcome email:
src/inngest/send-welcome-email.ts
const sendWelcomeEmail = inngest.createFunction(
{ id: 'send-welcome-email' },
{ event: 'clerk/user.created' },
async ({ event }) => {
const { user } = event.data;
const { first_name } = user;
const email = user.email_addresses.find(e =>
e.id === user.primary_email_address_id
).email;
await emails.sendWelcomeEmail({ email, first_name });
}
)
Now, you have a function that utilizes the same Clerk webhook event for another purpose. Clerk webhook events can be used for all sorts of application lifecycle use cases. For example, adding users to a marketing email list, starting a Stripe trial, or provisioning new account resources.
Sending a delayed follow-up email
To send a follow-up email, you can use the step.run()
. This method will encapsulate specific code that will be automatically retried ensuring that issues with one part of your function don't force the entire function to re-run. Additionally, you will extend the functionality with step.sleep()
.
The code below sends a welcome email, then uses step.sleep()
to wait for three days before sending another email offering a free trial:
src/inngest/send-welcome-email.ts
const sendWelcomeEmail = inngest.createFunction(
{ id: 'send-welcome-email' },
{ event: 'clerk/user.created' },
async ({ event, step }) => {
const { user } = event.data;
const { first_name } = user;
const email = user.email_addresses.find(e =>
e.id === user.primary_email_address_id
).email;
// Wrapping each distinct task in step.run() ensures that each
// will be retried automatically on error and will not be re-run
await step.run('welcome-email', async () => {
await emails.sendWelcomeEmail({ email, first_name })
});
// wait 3 days before second email
await step.sleep('wait-3-days', '3 days');
await step.run('trial-offer-email', async () => {
await emails.sendTrialOfferEmail({ email, first_name })
});
}
)
Next steps
To continue learning about how to get the most out of Clerk webhook events, check out the following:
- Platform guide: Consuming webhooks
- Guide: Fan-out (one-to-many)
- Guide: Parallel steps
- Reference:
step.run()