Handling webhooks with Inngest
Webhooks allow you to synchronize data from Clerk to your application backend. You can either handle them directly in your backend with an endpoint or use a tool like Inngest(opens in a new tab) which receives the webhook events for you and reliably executes functions in your codebase. When handling webhooks, Inngest receives the webhook events for you and uses a built-in queue to reliably execute longer running functions with additional functionality including:
- Limiting concurrency(opens in a new tab) to handle spikes in events without overwhelming your API or database.
- Triggering multiple functions from a single event (fan-out jobs(opens in a new tab)).
- Delaying code(opens in a new tab) to run after a period of time.
- Debouncing events(opens in a new tab) to reduce duplicate processing.
In this guide, you'll learn how to set up Inngest to receive Clerk webhook events and how to define Inngest functions in your application using Clerk events including synchronizing data and sending welcome emails.
To follow this guide, you need an Inngest account (free tier is enough) and have Inngest set up(opens in a new tab) in your codebase.
Setting up the Inngest webhook
To create an Inngest webhook endpoint and add it to your Clerk account, open the Webhooks(opens in a new tab) page at the Clerk dashboard. 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(opens in a new tab).
Viewing webhook events within Inngest
After setup, as webhook events are sent from Clerk to Inngest, new clerk
events will appear in your Inngest dashboard(opens in a new tab). Event names will be the prefixed clerk/
followed by the event name. See webhook events for a full list.
Creating a function to sync a new user to a database
With Inngest already set up in your codebase, you can now use these events as triggers for your functions.
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.tsconst syncUser = inngest.createFunction( { id: 'sync-user-from-clerk' }, // ←The 'id' is an arbitrary string used to identify the function in the dashboard { event: 'clerk/user.created' }, // ← This is the function's triggering event async ({ event }) => { const user = event.data; // The event payload's data will be the Clerk User json object 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(opens in a new tab).”
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.
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/welcome-emails.tsconst 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 // `emails` is a placeholder for the function in your codebase to send 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
Every Inngest function handler has an additional step
object which provides tools to create more complex functions. Using step.run
allows you to 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, other tools like step.sleep
(opens in a new tab), are available to extend functionality.
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/welcome-emails.tsconst sendOnboardingEmails = inngest.createFunction( { id: 'onboarding-emails' }, { event: 'clerk/user.created' }, async ({ event, step }) => { // ← step is available in the handler's arguments const { user } = event.data const { first_name } = user const email = user.email_addresses.find(e => e.id === user.primary_email_address_id ).email await step.run('welcome-email', async () => { // `emails` is a placeholder for the function in your codebase to send email 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 }) }) } )
Now, you've extended the usefulness of Clerk webhook events even further to build an onboarding drip email campaign in just a few lines of code.
Testing webhook events using the Inngest Dev Server
During local development with Inngest, you can use the Inngest Dev Server(opens in a new tab) to run and test your functions on your own machine. To start the server, in your project directory run the following command:
terminalnpx inngest-cli@latest dev
In your browser open http://localhost:8288 to see the Inngest Dev Server.
To quickly get events to test within Dev Server, you can select any individual event from the Events tab then select the Send to Dev Server.
You'll now see the event in the Inngest Dev Server's Stream tab alongside any functions that it triggered.
From here you can select the event, replay it to re-run any functions or edit and replay to edit the event payload to test different types of events.
Conclusion
Congratulations! You've now learned how to use Inngest to create functions that use Clerk Webhook events.