Handling Clerk webhook events

Clerk logo and graphic showing 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.

The Webhooks page in the Clerk Dashboard. A red arrow points to the button for Add Endpoint.

On the next page, select the "Transformation" template tab and the Inngest template, then click on the "Connect to Inngest" button.

The Webhooks page in the Clerk Dashboard showing the Inngest transformation template. Red arrows point to the Transformation Template tab, the Inngest template, and the Connect to Inngest button.

A popup window will appear to complete the setup. Select "Approve" to create the webhook.

The Inngest permissions popup window showing the Approve button.

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.

The Webhooks page in the Clerk Dashboard showing a connected Inngest account. A red arrow points to the Connected button.

To complete the setup, scroll down and select "Create".

The Webhooks page in the Clerk Dashboard showing the end of the page to create a new endpoint. A red arrow points to the Create button.

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: