In this chapter, our goal is to synchronize our clerk accounts with our users database. In order to do that, we're going to have to implement our very first webhook. In order to do that, we're going to need to establish a local tunnel. There are a lot of solutions online for local tunnels, but I would highly recommend using ngrok. It is by far the most reliable method of doing this.
And one very, very cool thing is that they will give you a static domain, right? So this is very useful because usually every time you start a local tunnel using some different tools, they give you a different URL, which means that you have to change your whole configuration in all the websites where the webhook is coming from. But if you use a static domain, you can just remember it once, put it once, and it's always going to work. Also, just to clarify, we're going to need ngrok only for development. In production that's not going to be a problem because we will have our URL, our website.com, but in development mode we don't have a URL.
We cannot tell a production app to connect to our local host app. That's why we need to expose our local host app to the public and for that we need something like ngrok. So again, ngrok is not required, static domain is not required, but both of these make development much much easier. Let's go ahead and let's do that first. So I'm gonna go ahead and create an account on ngrok right here.
Once you're logged in you're going to see all the instructions you need. For example, on Mac OS, which it recognized, I can install it using brew and then I can add my token. Keep in mind, again, you should not share this token with anyone. This is my dummy account, which I just created and I did not even connect this to my PC but you should not share this with anyone right or you can just use the download button and the same thing is true if you're using Windows or Linux or any other solution. Once you have ngrok installed, you should go ahead and confirm that.
So let's go ahead and check my version. So ngrok 3.19.0 is my version. Basically, ngrok should not throw you any errors. It should work as a command. The next thing you have to do is you have to establish a static domain.
So instead of your ngrok dashboard here, let me zoom in a bit. You should find domains and go ahead and click create domain. And inside of here, you're going to get one, only one free static domain. So for each account you create on ngrok, you're gonna have this static domain like this. So now if you go ahead, for example, and do the following.
Yeah, this is not gonna work for me. So for you, this is going to work. So you can just copy whatever domain you have here, but I have to use a different one, because as I said, this is my dummy account. I didn't connect this. So let's do this.
In one terminal, run your application on localhost 3000. And in another, go ahead and run that ngrok command. Let me just go ahead and... Why can't I do this? Okay.
So ngrok. And it seems like I have to do this. So let me just ngrok like this, and I have to modify my URL. And in case this doesn't work for you, don't worry because I will show you one little trick. So if I go ahead and change this to my proper domain and change this to look at the port 3000 because we are running on 3000 here in another terminal, it will start my tunnel.
But when I was developing this, I had a bug that if I typed dash dash URL, I kept getting an error that URL doesn't exist. In that case, you can try dash dash domain and it should still work, right? So it looks like they have changed that name but they still have backwards compatibility. So it will depend on the version of ngrok you have. So if ''url'' throws an error you can use ''domain'' and you will get the same thing.
So go ahead and run this and now if you go ahead and expand this, you should be able to see the URL where your application is exposed to the public. So right now, if I go here, you will see that it will load the app which we have been building previously. There we go. So, newtube with our basic layout here. Perfect.
So, just confirmed that this is working, confirmed that you can visit this new URL here. Let me go ahead and just enable developer mode here. There we go. So, this is my URL, evolve the humbly gopher, whatever that means, it's random, right? Great, so one thing that's kind of a hassle in development is that you have to remember to start this every time you need local development.
But I found a neat little trick you can do. And in order to do that, we first have to do, we have to install concurrently. So let me just show you the npm version of this. So concurrently enables you to run multiple commands concurrently. The issue is you probably think okay well I know how to run 10 commands but that's not running them concurrently and attempting to run a webhook and npm run dev at the same time will fail.
So that's why you need concurrently. So let's go ahead and add concurrently with version 9.1.2. So I'm going to shut down both of my apps here and I'm gonna do bun add concurrently at 9.1.2. There we go. And then what I'm going to do is I'm going to go inside of my package.json.
You can confirm that you have concurrently installed right here. And then go ahead and do the following. Add that webhook like this. And then webhook is going to be your ngrok command. Let me try ngrok.
There we go. So basically this command which you just previously run, right? Make sure that the port is 3000 and make sure that you're using the proper dash dash URL or dash dash domain. And simply add that here to be dev webhook. So now if you actually try and do bun run dev webhook, it will start the ngrok.
Great, so that's great. That's now shorter. You didn't have to remember the whole domain thing, but let me show you another little trick you can do. You can add dev all like this. And then you can run concurrently.
We can run it like this because it is installed inside of this project, right? Otherwise you would have to have it globally installed. So concurrently, and then you have to add a backwards slash because you have to escape this regex. And inside of here, go ahead and run bun run dev webhook. So that's the First one we're going to run.
And we have to escape this like this. This is a bit confusing, but yeah, backwards slash quotes and backwards slash and then add a quote. Basically we have to escape this quotes, right? And then you can just go ahead and do the same thing again. So backward slash and then bun run dev and then backward slash and close this.
So now it's going to run together the webhook and our next dev. So if you go ahead and try it now, let me exit this and do bun run dev comma colon all, it will concurrently run the webhook and bun run dev. So if I go ahead and try going on my evolved humbly go for webhook. You can see that it should be fully available. It's refreshing now.
There we go. So no errors. This route definitely exists and I can log in here. Great. And if I go inside of my local host here, local host works as well.
So both of my apps are now working with one command. So that's why I wanted you to have a static URL with ngrok because it's super simple. Again, you can change this to domain if URL is not working for you. And if you forgot where I got this from, it's right here, right? So inside of your ngrok domains, and you have one single domain here, you can click start a tunnel, and then you will get this command right here.
Just make sure to change the port from 80 to 3000 like this. So this should work because ngrok is globally available. So if I type ngrok, you can see it's globally available. Concurrently is not globally available for me, as you can see, but it works here because we have it installed in our project. And yeah, technically we can move this to dev dependencies.
I should have probably installed it in dev dependencies, but yeah, it's okay. It's not gonna be a problem if it's in my dependencies. It's not too big of a package. Great. So we now have that.
Now, let's go ahead and let's do the following. We have to go inside of a clerk dashboard. We have to go inside of configure right here. And we have to go inside of our webhooks. So let me just find the webhooks.
There we go, right here. And now we have to add an endpoint. So this will be our URL right here. So let's just add HTTPS, your static URL, which you got from ngrok, slash, this is going to be API slash users. I think that that's where we're going to put it.
Please excuse me for a second. I'm just confirming that with the original source code. All right, let's also add slash webhook here. So it is a bit more precise. So slash API slash users slash webhook.
Make sure it is like this. So this will be the equivalent to localhost 3000 slash API webhook, right? In case you're confused. But just make sure that inside of here, you have to put your actual local tunnel because this app has no idea what localhost is, right? That's only on yours.
Great! And now inside of here, if you want to, you can add descriptions. For example, user synchronization. You can write that if you want. And then inside of here, you can go ahead and find what you need.
In our case, I need everything user-related. So created, created at edge, deleted and updated. I don't really need anything else here. Great! And let's just click create And what's going to happen now is that you can always, of course, change this URL if you accidentally wrote the wrong one.
And now let's go ahead and copy this sign in secret. Right. So you have to click on the little eye icon. You will see the secret again. Do not share this secret with anyone.
Go inside of your .environment file, and I'm just going to add this with my clerk secrets. So signing secret will be my new signing secret like this. There we go. How about we prefix it with clerk sign in secret? And yeah, no need for next public in front of this because this should not be exposed to the client.
This should be on the back end only. Great! And now what we have to do is the following. We have to take a look at the documentation for the webhooks. You can find them right here in the webhooks section here.
And you can see and learn more about how they work, why do they use Spix, some payload structure. And if you scroll a bit down, you're gonna find synchronized data to your database. And you can find a little guide here. This guide will give us a little template for the API route, which we need to create. And you can see, they also use ngrok precisely because it has this very useful domain, right?
This very useful static domain. So we already did all of these steps. That's great. We didn't have to do that. We even created, you know, this cool little dev all which starts both the webhook and the project at the same time.
But now we have to set up the webhook, which we actually did, and we subscribed to all events for the user, not just user created. We added the sign-in secret to the environment local, so we did this, that is fine. And now it says to set the webhook route as the public in your middleware. We don't have to do that, right? Because remember, our middleware by default allows all routes, right?
So I can only protect that route. I cannot make it more public than it already is. Now let's go ahead and let's add SWX. So let me go ahead and click here. I think this will open NPM.
It looks like 1.45.1 is the current version. So let's do one, add SWX at one point. I already forgot 45.1. There we go. Once we have Swix, let's take a look at what else we need.
So we need to create the actual endpoint. All right, so they have their own endpoint here. We're gonna do a different one. So let's go inside of source instead of app folder and I didn't really demonstrate how to create routes in Next.js, that's something I forgot, but it is as simple as creating client routes, right? Instead of having page file name you have a route.ts file name.
So for example, if you wanted to create a test like this inside the route.ts, you could very simply write export const Get and return response. Can I just write hello? I think this should work. So if you now go to localhost 3000 slash test, Do I have my app running? I do not.
Bun run dev all. So from now on, you're gonna use bun run dev all, not bun run dev, right? And there we go, you can see how it works. So that's how you create an API route inside of the Next.js app router. Very simple, right?
So remove this test for now. And now where I'm going to put my API routes is not required, but I quite like them to be inside of an API folder. So yeah, it's not required, but I think it makes sense to keep them here. And now let's go ahead and let's create our users and then let's create our webhook right here, right? So this is basically slash API slash users slash webhook.
So it needs to match whatever you wrote here. Slash API slash users slash webhook. And inside of here add a route.ds file. Great. And now we can copy this example they have right here.
In case you decided not to follow the documentation, right, or just want to see me typing, don't worry. I'm going to pause the screen so you will be able to see each part if you want to do it like that. So we first import webhook from SWX, headers from Next headers and webhook event from Clerk Next.js server. Inside we export an asynchronous post function, So similarly to how I just demonstrated the get function, that's how you do post function here. One thing which we immediately need to modify is the process environment signing secret.
That is because inside of our dot environment right here I've named it clerk signing secret. Just in case we have multiple signing secrets, I think it makes sense to add a prefix here. So I'm going to change this process.environment to use clerk signing secret and I'm going to modify this warning here to add clerk signing secret to .environment or .environment local. The rest of the naming which is signing secret does not matter because it's a constant. All that matters is that it reads the clerk's signing secret here.
Great! Inside of here we initialize the new webhook using that signing secret. Again the webhook comes from civics which they use to retry webhooks and have a very reliable way of using them. We obtain all the necessary headers which we are going to use to validate this request because this request will be coming not from a user but from a random server. So we need to validate that this request is actually clerk telling us, hey new user was created what do you want to do with that user?
We don't want anyone else to spam us. So if any of these things are missing from the headers, it most likely means that someone is just trying to access this endpoint and it's not a webhook handler. If we pass this validation, we are going to get the body. So we are going to get the JSON version of the body, but we are also going to get the text version of the body. And then we are going to verify our webhook with all this information above.
The Svicks ID, the timestamp and the signature all combined with our unique signing secret. So this is a very very secure way of only allowing our specific instance of Clerk webhook from contacting our application. No one else can force themselves and trigger this webhook and thus manipulate our data in an unwanted way. So that's why we are doing this kind of validation. If this fails, we throw an error and we say I cannot verify that this is who you think it is, I'm just going to break the function.
Otherwise, we can finally access our events here. So you can see that we have received the webhook with id id and the event type of event type like this. So I believe that clerk might allow you to test this out. I'm not sure Is there a way to like test it out? Let's see, maybe event catalog logs.
If I go inside of user, user created. Okay. I thought that maybe they're going to give me some. Yeah, I have no idea. Oh, okay, user created.
Okay, we can see, well, we can see the body, but not exactly what I want. All right, this is what we are going to do now. We can remove or you can leave this console logs if they help you understand but this is what I'm going to do. I'm going to check if event.type, my apologies, if event type is identical to user.created, you can see we have autocomplete here because of TypeScript. In that case, what I'm going to do is I'm going to await database which we can import from database.
This comes from our recently created Drizzle database here. So we're going to call that database and we're going to insert into users. You can import users from database schema. This constant which we have exported right here. Make sure you have exported it as well.
Into users we are going to insert the following values. First of all we're going to give it a clerk ID of data.id. My apologies we don't have data here. Yeah because it's only the structuring ID here. I don't like that so I'm just gonna call this data like this and then this can be data.id if that's easier for anyway, right?
So data transfer, my apologies, data.id will be the clerk ID like that. Name here will be data first underscore name and data last underscore name. And image URL will be data image URL. Now we're getting some errors here and I believe that's because data can be all of these things but we know that it's going to be user JSON if it's inside of this type of webhook. So what I'm gonna do is let me see can if I move it here there we go looks like that solves the issue because if you read the data inside of this if clause, it knows more precisely what it is.
So it's a user JSON, whereas If you move it outside, it can be all of these things. Great, so just move it inside. And well, you can remove this console box. So they don't cause any issues. Great, so we now have the data from event.data.
And yeah, if you want to, you can destructure that like this. And this is why I told you to choose Google as the only sign in option, because if you chose email, I think you're not going to have these properties. So you're not going to be able to create a nice name for your user, right? So if you use Google login, I think you will almost certainly have first name and last name. Otherwise, all of your users will be called null null, because this can be null.
And since we are using this inside of template literals, it's just gonna show you null null. So if that happens, it's most likely because of the provider you're using. So what you can do, if you really care about it, you can do a name, and then you can check if data first name is missing. In that case, you can use data email addresses and then get the first and then get the email address. But as far as I know, this can also be null maybe or maybe not.
Yeah, basically just be careful with the provider you chose. All right, so this is enough for the user created. Now let's do event type user deleted. I'm gonna do the structure data from the event here. And I'm going to do await database delete users where, and now we have to use drizzle ORM, and we have to import equals from Drizzle ORM here.
We're very simply going to target whatever the users.clerkId has data.id. Let's just see if I did this correctly. So Data.id can be string or undefined. All right, so this is what I'm going to do. If for any reason whatsoever, we cannot access data.id, we can only throw not an error, but we can return new response.
Let me see, how do I throw an error? All right, I just specify the status. And I will just say missing user ID like this. So if we are, for any reason we don't have this, which I don't know how exactly can happen, we don't know which user to delete. Because remember, when we created the user, we stored that data.id, which in here clearly always exists, and we store it inside of clerk ID field.
So we have to find by that clerk ID, whatever we have in the current data ID. So now, one more thing that we need is if event type is user updated like this. In that case we're going to do await database update users. Go ahead and set the name and you can just copy, well, you can just copy two of this, right? So the only thing we are not going to modify, let me restructure the data from the event.
The only thing we're not going to modify is the clerk ID. Instead, we're going to use it to specify which user to update. So, Perk ID field and we query by data.id. There we go. So that is our webhook.
Make sure to return the new response, successful response at the end. Otherwise, all of your webhooks will fail by default, right? So this is quite important that you have that here. And I believe this is it. Let's go ahead and try it out now.
So this is what we have to do. Go ahead and make sure you have Bundle Run Dev All running. Make sure that you can go ahead and find this static URL. Make sure that you can visit it and it should load your app, right? This URL should not fail.
Great. Now, the second thing we should do is the following. We should go inside of dashboardsclerk.com instead of your users and delete whatever user you have. So just completely remove them. What's going to happen now already, I think, is that it will attempt to go to slash API users webhook.
You can see that it got to API users webhook 200 right here. And you can see the status of that inside of configure. You can find the webhooks right here. What I think is going to happen now is that this webhook is failing I think. All right, so it looks like it is okay.
Let's see Why does it say that it's okay? Should have it failed. Let's see. Await delete users. Yeah, perhaps this doesn't throw an error.
It maybe just returns back null. All right. I mean, it's okay. We don't really have to throw an error if the user didn't exist in the first place. Now let's try and see do the other events work.
In order to try that out, go ahead and run Drizzle Kit Studio. So let me go to local.drizzle.studio. Right now I have zero users inside of my database as you can see. So what I'm going to do is go to localhost 3000. Don't go to your local tunnel.
So go directly to localhost 3000 here and go ahead and create a new account. I'm testing with Google so I know that I will have name and surname for this user. So let's go ahead and try it out. I'm getting created, there we go. And now if I go inside of my Drizzle Studio and refresh, hopefully, there we go.
We have a new user record and you can see it says one right here as well. So we have the clerk ID, the name John Doe, my image URL created at, updated at. We have all of those things right here. Great. So now what's interesting is that if you go ahead and take a look inside of here, you should see this successful route.
Let's see user created. So this was successful. Great. And now if you, for example, manage your account and update the profile picture. Let's go ahead.
I'm gonna try and upload something. So here's my new picture, right? And what will happen now is that I should get another post request API users webhook here, and I should also have another log here, user updated. As you can see, it seems to work just fine. So user has now been updated and inside of my Drizzle Studio, well, you won't really see any difference, but this image URL has been updated, right?
It is showing this image right here, which was not what was previously my image. So great. All we have to check now is does deleting the user also work? So let's go inside of dashboard here, instead of users, and let's delete this user from here. And now what should happen inside of my Drizzle Studio is that I no longer have that user here, and that is true.
Amazing, amazing job. So remember, From now on, every time you start your app, you're not gonna use bun run dev, you will do bun run dev all. This will be crucial, otherwise your data will not be synchronized. And then the best course of action is to just dump your database. Of course, this is only for development.
This will not happen in production because in production, this will just be your URL, whatever your app is hosted on. This is a solution for local development. This is the only way to do it, actually. There's no other way of doing this. Great, great, great job.
Let's see if we did everything we planned. So we created an ngrok account, we obtained a static domain, we added a script to concurrently run a local tunnel in our app, we created the user's webhook and we connected the webhook to clerk dashboard. That's it. We did chapter 5. Great, great job.