In this chapter, we're going to add subscriptions to our app. This will include creating the subscriptions backend schema, creating the subscription functions so that we can protect our API routes, creating a UI protection so users can't see pages which are only meant to be seen by premium users. And we're also going to add the dedicated billing page. All of this will be done quite easily thanks to clerk billing. But let's start with the simplest part, creating the subscriptions schema so we can synchronize convex database with clerk billing.
Make sure you have your app running, make sure you're on your main branch. Let's go ahead inside of our apps, let's go inside of packages, backend, convex and let's head inside of schema. And for my latest table I'm going to add subscriptions. Let's go ahead and define this table. In here let's set the organization ID to be a type of string.
Let's set the status to be a type of string as well. And now let's just add an index here. By organization ID using the organization ID field. That's the only thing we need. Make sure that you visit your back-end app here and just confirm that it compiles normally and it pushes the table.
Now that we've added our backend schema, let's enable billing on Clerk. So head to the Clerk dashboard and in here you will have a subscriptions tab. If you don't have it here, head inside of configure and scroll under the billing section. In here, head inside of the settings, so billing settings, and let's go ahead and make sure that we enable this. So let's see, inside of subscription plans, if we click get started, that will enable the subscriptions, I believe, or if you click here.
I think basically you need to create a plan and then this will consider this will be considered as enabled. So let's go ahead and create a plan here. And let's go ahead and let's add an organization plan. Right. So we are working with plans for organizations.
Now this will only appear for you if you have organizations enabled. Right. If you don't have organizations enabled, I think you will only see the option plans for users. But since we are building a B2B app, our subscription will work per organization. Right.
So each organization will have their subscription plan. So yes the we are going to have one which is free. It was automatically created. Now let's click add plan and let's add a premium one. So this will be called Pro.
The key can stay Pro and in the description I'm not sure we have to add anything. Let's go ahead and let's add 29 as the monthly base fee here and let's just click Save for now. There we go. So now inside of your subscription plans here you should have the free plan and you should also have the pro plan. Now that you have that make sure to click enable billing here if you have that button make sure that you see the message billing is enabled.
You can also double check in the settings just to see that billing is enabled right here and make sure that you have clerk payment getaway selected. Excellent. Now that we have that, Let's go ahead back inside of localhost 3000 here. And let's go ahead and let's develop a very simple pricing view. So the pricing view will be here instead of plans and billing which currently is just an empty billing page.
So let's go ahead and find that. So billing page.tsx. Here we go. Instead of apps, web app dashboard billing page.tsx. So let's go ahead now inside of our modules let's create a new folder called billing inside of here let's create UI views and let's create BillingView.tsx let's mark this as useClient and export const BillingView inside of here let's return a div with the class name flexMinimumHeightOfScreen flexColumn, backgroundMuted and padding 8 Another div inside with the class name mx-auto-full-width-maximum-width-screen-medium Another div inside with the class name space-y2 an h1 element plans and billing and a class name text to Excel and the text for Excel.
Below this heading let's add a paragraph. Choose the plan that's right for you. And you can also replace this with appos like that. Now let's go ahead and back inside of this page and let's actually use the billing view. Import.
There we go. Plans and billing, choose the plan that's right for you. Instead of the billing view we now have to build the pricing table. So this is how we're going to do that. Outside of this div let's start a new one and give it a class name margin top of 8 and inside pricing table.
Let's not import this from anywhere. Instead, we're going to create the components folder inside of this UI. And let's add pricing-table.tsx. Let's mark this as use client as well. Let's import pricing table from clerk next JS as clerk pricing table so we alias it.
The reason we are aliasing it is because we are going to have a constant called pricing table here. So if we didn't alias it, this is a conflict. So that's why we are aliasing the import. Now in here, let's go ahead and let's return a div, class name, flex, flex column, items center, justify center, gap y of 4. And let's add the clerk pricing table.
Let's add one more thing here for organizations. Now let's import the pricing table not from clerk-next.js but from our components pricing table. Let's refresh And there we go. You now have the free tier and you have the new pro tier that we created. And if you click subscribe, you will see how easy it is to subscribe.
I would recommend not doing it right now simply because we are going to test the free tier now. But if you accidentally subscribe, no worries. Just switch to an organization where you don't have the subscription or just create a new organization as simple as that. Just make sure you are testing on some organization that doesn't have pro. So it's easier for you to look at the free tier and how it's going to look like.
Excellent. Now in order to make this look a little bit better, what I suggest we do is we go inside of the Clerk here, go inside of my apologies, inside of Configure, Subscription Plans, and let's go inside of Pro. And Let's go ahead and give it some features here. So it will look better. So for the features, let's add AI customer support Let's click create feature Let's add AI voice agent And just to put some ease of your mind, this isn't required.
So yes, you can use this in a much better way than I'm doing it. I'm only doing it because when you add features and when they are marked as publicly available and when you refresh your pricing table, they will appear here. That's the only reason I'm doing it. Usually you could do it like if you had multiple plans and some plan offers voice agent, another plan offers AI customer support and then you would pick and choose what feature the organization has. So yes, you could technically just look for individual feature that some user has.
But in my case I'm just using it as a nice UI. So phone system, let's add that feature. Knowledge base, let's add that as well. And let's add team access because remember even though we're using clerk organizations we have limited our organization users to just one user. Let's revisit that quickly.
Organization management settings I believe. There we go. Default membership limit, limited membership 1. Why 1? So keep this as 1.
Why 1? Well, very simple because Clerks billing, let me see if I can find the exact, okay, so these are the docs. But basically Clerks organization has its costs only when it has two or more monthly active users within an organization. So for that reason, if we just allow users to create new organizations for themselves and be alone inside, This does not make clerk costs any higher. So in this specific scenario, these organizations are completely free for us and for our users.
So in order to allow our users to invite new people, so if I go ahead and add Antonio at example.com you can see that this will now fail because we have a limit of one so only once they upgrade only once we are actually making money from this user does it make sense to allow them to invite new people because that will occur more costs on our side. So this is how we're going to make sure that our third party integration in this case, Clerk is self-sustainable and actually profitable for us. So that's always something that you have to think about. Great! So let me just refresh in here so we can see all the new features that we are going to get here.
Amazing! And yes you can also add some things in the free plan if you want to, but I think this kind of looks okay for now. Great. So now let's go ahead and just style these cards a little bit better instead of the pricing table here. So let's add appearance, elements, pricing table card, shadow none, border, rounded, large.
Pricing table card header, BG background, And let's copy this a few more times. The next one will be pricing table card body and pricing table card footer. There we go. Now this kind of looks like our style. Now what I want to do is I want to.
Now actually I'm okay with as it is right now. I wanted to change the color of this but I'm trying to think of the best way to do that. I think that we have to go inside of the layout, inside of our web application and we have to find the clerk Provider. And in the Clerk Provider here, there should be an option to add appearance variables and change the color primary. Now it cannot be a tailwind color, it needs to be a hex color.
So that is 3C82F6. And then there we go. Now the entire clerk is themed in our color so I like this better. Great so That's the billing page finished for us. But now it would be nice if we could, you know, prevent users who are on their free tier from visually even seeing the widget customization, the voice assistant, right?
I want all of that to kind of appear blocked or locked for free users. And they have an option to get redirected to this pro plan and then they have to upgrade and then they'll have access to the knowledge base and such. So let's go ahead and figure out how we can do that. So the way that we can protect the UI is actually very, very easy, again, thanks to Clerk and the fact that we are using their billing. So if you go inside of web app dashboard and let's see what do we want to protect.
Well I think we can start with files because it's the first thing here, the knowledge base, right? And let's go inside of its page.tsx And in here I'm going to import protect from clerk next JS and that's all I'm going to do for now actually. And I'm going to modify the return a little bit. So this time I will return protect And I will render file as view inside. And the condition will be if has plan pro.
So how do I know I need to type pro here? It's very simple. It is because inside of my billing subscription plans, let me refresh here so I can find my pro plan, find the key. So The key is what's used in your code base to refer to this plan. So if this was Pro123 I would have to write Pro123 here.
So check what the key stands for. If you wrote it exactly as me it should be pro and the capitalization matters. So make sure that you write it like this. So if the user has I mean if the organization has planned pro then show the files of you. Otherwise let's go ahead and let's show you need to upgrade like this.
There we go. You need to upgrade, right? But if I change this to, let me see, what is the name of our free plan right so we always have to check that inside of free free underscore org. So let me check with that and now I can again see the knowledge base because I change this to be the free plan but let's keep it pro for now. And now let's make a little bit of a nicer overlay here so that this kind of a lock screen looks better.
In order to do that let's implement the feature called the premium feature overlay. We're going to do that inside of the billing modules. Instead of UI components let's add premium feature overlay dot t s x. Let's mark this as use client and let's import a bunch of things from Lucid React starting with type lucid icon. And now let's add all the other icons we're going to need.
So book open icon, bot icon, gem icon and now let's add microphone icon, palette icon, phone icon and users icon. Let's add user router from next navigation. Let's import the button from Workspace UI Components button, and let's import everything we need from our card, including the card, content, description, header, and the title. Now Let's create some types so we have modular display of the features that user will unlock when they upgrade. So interface feature will have an icon of lucid icon, label of string and the description of string.
And let's create another interface, premium feature overlay props will only accept children now let's go ahead and create a constant called features which will be a type of feature and an array of those like that Now let's go ahead and export const premium feature overlay. Let's assign the props. Let's extract the props. Now let's render something here. So I'm going to render a div with a class name relative and a minimum height of screen.
Then I'm going to create blurred background content. This will render the children. And I'm going to give this pointer events none, select none and blur two pixels. Below that I'm going to add an overlay. An overlay is going to be a self-closing div.
It's going to have a class name of Absolute, inset 0, background black with 50% opacity, Backdrop blur 2 pixels. And that's it. And now in here I'm going to add the upgrade prompt. So I think that already we should be able to see this. So let's quickly go back inside of the page and let's see how we are going to use this.
So let's import premium feature overlay from modules billing UI. And instead of the fallback, let's do the following. Render premium feature overlay and then inside render the files view like this. And now this is what will happen. We kind of give the user a sneak peek of what's behind, but we don't let them interact with this.
And if you think, oh, but isn't this a security issue? We are rendering the component. No, rendering UI things shouldn't ever allow your user to actually do something. Our API routes, our functions are very well protected. I mean, you definitely know that we check the user identity and the organization ID in every single private API route or API function if you want to call them that way.
So you don't have to worry about importing or rendering a component that a premium user shouldn't see. That's perfectly fine, right? We're just going to check on the back end if they're allowed to do this or not. But yeah, this is what I kind of want to do. I want to give them a sneak peek and then I'm going to give them a prompt if you want to see what's fully behind upgrade.
And that's what we're going to do now. So the upgrade prompt will be a div with a class name absolute inset 0 z-index of 40 flex items center justify center and padding 4. Then let's add card class name full width maximum width medium then let's add card header Let's give the card header a class name of TextCenter. Let's create a div with a class name FlexItemsCenterJustifyCenter and another div with a class name, margin-bottom of 2, inline-flex, height 12, width 12, items-center, justify, center-rounded, full, border-background, muted. And inside render the gem icon which we previously imported.
Give the gem icon a class name of size 6 and text muted foreground. There we go. So a little model like component with a gem which represents kind of this is premium right. And outside of these two divs still instead of the card header add the card title which will say premium feature and give this a class name text extra large. Then below that card description this feature requires a pro subscription.
There we go. Outside of card header, add card content with a class name space Y6. And in here we're going to render the features list. So this will be a div with a class name space Y6. And now let's add at least one feature in here in the features array.
For example, let's add AI customer support. So object, icon, bot icon, label, AI customer support, description, intelligent, automated responses 24x7. Now that we have that, Let's go ahead inside of here and let's iterate over our features. So features.map, get the individual feature and let's go ahead and render something inside. So this will be a div with a key of feature.label class name flex-items-center-gap-3 inside of here a div with a class name flex-size-8-items-center justify-center-rounded-large-border-and-background-muted inside let's render feature.icon Size 8, items center, justify center, rounded large, border and background muted.
Inside let's render feature.icon and a class name size 4, text muted foreground. There we go, So first feature is starting to appear. Outside of that div let's create a new one with the class name text left. Inside of that div let's go ahead and create a paragraph which will render the feature label and another paragraph which will render the feature description. Now let's style those paragraphs.
The first one will have a class name font-medium and text small. And the second one will have a class name text muted foreground and text extra small. Great. Now outside of this div still inside of the card content let's add the button let's say view plans and let's give some attributes to the button starting with the class name full width on click for now an empty arrow function and size of large now in order to make this button redirect to somewhere, let's prepare our router hook here. Const router, use router.
We already have the router here imported from Next Navigation. And now what we have to do is router.push billing and there we go. Now you can click here and you get redirected to the billing and now let's just go ahead and add as many features as we want here so the next one can be a voice agent feature. So use the microphone icon AI voice agent with this description. You can pause the screen and then copy with me if you want.
Phone icon, phone system, inbound and outbound calling capabilities. Then let's add the knowledge base. Book open icon, knowledge base, train AI on your documentation. Let's add the team access now. So icon, users icon, label, team access, description, up to 5 operators per organization.
Now let's add widget customization. Customize your chat widget appearance using the palette icon. And now it should look like this. If you think it's too much, you can of course remove some of them because they will see the plans here again. I just think It's kind of worth it to create a separate component for this.
It looks kind of cool, at least in my opinion. And now we just have to repeat this for widget customization and for voice assistant. So let's go ahead and do it inside of the widget customization. So customization page. So that's inside of dashboard, customization page.tsx.
Let's import premium feature overlay, let's import protect from clerk next JS. And I'm just going to copy this entire thing here and replace it here like this. And I'm just going to replace the files view with the customization view. And now there we go. You can see that this is a protected premium feature now.
Yeah, this doesn't look the best that you have to scroll. Yeah, but I'm pretty sure you can fix that somehow. Maybe disabling the scroll within the premium feature overlay would be one way to do it. Not sure. Yeah.
But at least something to give the users a sneak peek and then like click here to upgrade right. And we also have to do that in the voice assistant right here. So I'm going to copy this thing again. Let's go inside of wapi-page.tsx. So instead of plugins wapi-page.tsx.
Okay, let me just remove the double return And now let me just import protect from clerk-next.js, premium feature overlay and replace file as view with wapi-view. So now we have a nice little reusable way to protect our UI things, right? So only premium users should be able to see these things. And if you want to test it out, let's try and subscribe. You can just click pay with test card if you're in development mode, of course.
And there we go. You can see it's instant. And let's try and click inside of knowledge base. I just realized I have a typo, knowledge base. But yeah, you can see now it works.
You can see them. And if you switch to some organization that doesn't have the Pro tier, They can't access it. So our billing is scoped per organization. B2B app. Exactly what we wanted.
So let me just fix the knowledge base issues sidebar dashboard that sidebar. Knowledge base. There we go. Perfect. Excellent.
So now if you're wondering where does this subscription appear, it appears in your clerk subscriptions tab right here. There we go. Revenue 29, monthly recurring revenue 29. This organization is now on ProPlan. Perfect.
So we did the subscription schema and we added the UA protection and we added the billing page. Now let's develop the subscription functions. So usually how I would attempt to do this, how I would attempt to connect a convex backend with clerk subscription state is by visiting the JVT template, right? Convex right here. And then in here, I would search for organization subscription.
Right? But as you can see, that currently doesn't exist. So it's not something that we can add in our token. It would be nice if we could kind of add if organization is on pro plan because if we could do that we technically wouldn't even have to have our subscription schema table right. But because of that reason because we can't add them We have to manually keep track of when user subscribes.
So yes right now we actually Subscribed but we never actually created the subscription table because we don't have the webhook set up but we will do that next. Let's first finish the subscription functions. So I'm gonna go inside of my packages backend convex system and in here I will create subscriptions.ts. Let's import v from convex values, let's import internal mutation and internal query from generated server and let's export const absurd which will be internal mutation. The arguments this will accept is organization ID which is a type of string and status which is a type of string.
Let's go ahead and set the handler here. In the handler we accept the context and the arguments and let's check if we have the existing subscription by using await context.database.query subscriptions with index by organizationID and let's query organizationID arguments organizationID and let's pick the unique one. So only one organization, only one subscription per organization can exist. If existing subscription is true, meaning if it exists, what we're going to do is we're going to patch it, we're updating it then. So let's patch it and let's simply update the status of that organization.
Otherwise, we are creating it for the first time. So we are inserting inside of subscriptions with organization ID and with status for the very first time. So this is the function that we are going to use to create or update the status of someone's subscription through our webhook. But we need to prepare that here in the internal mutation so that we can actually call it within a webhook. Now that we are here, let's also attempt to fetch a subscription internally.
So export const getByOrganizationId, internal query, arguments, organization ID, string handler, asynchronous. Let's get the context and the arguments and we're going to do a very simple thing. This. So let's just copy and paste it here. In fact, we can return await context database subscriptions by organization ID arguments organization ID unique.
So this is going to be our internal query that we're going to use throughout our other API routes to confirm whether this organization that the user has in their identity token has subscription or not. Perfect. So right now if you go and visit your convex.dev here, and if you log in, you will see that your dashboard subscriptions table is completely empty. Right. So make sure you're inside of app here.
Make sure that you have this open the tables click inside of subscriptions and you will see it's empty because we never ever create a subscription. So how do we create subscriptions. Right. We now have the functions. But how do we actually create the subscriptions when someone upgrades for the first time.
Well, we do that using webhooks. So in order to set up webhooks within Convex and connect them to Clerk, there are a couple of steps we have to do. And you can find a very, very good boilerplate example by heading to convex docs and search for clerk and webhook. They don't have the exact thing that we need here, but they have a pretty similar one, which is storing users in the convex database. So we don't really have a need for this but if we did we could also leverage the webhook for that but in here if you scroll after their initial thing so what do they do here first they create the user stable schema You can think of this as our subscriptions table, right?
Then they create mutations. We did this too right now, instead of the system subscriptions, the internal queries, right? And now we are skipping this, we're skipping this, we're skipping this, and scroll down until you find setup webhooks because this is what we are interested in. So in order to set up webhooks, usually you would need to somehow start a local tunnel to give Clerk webhooks access to your app. But Since we're using convex, convex is already a cloud.
Convex can already contact Clerk and vice versa. So all we have to do is find our convex webhook endpoint. So in order to do that, we have to find the .site URL. And there are a couple of ways you can do that. In here, you can see your deployment name in the .environment local file in your project directory or your convex dashboard as part of the deployment URL.
So let's go ahead and check inside of our convex folder here. In the backend we have .environment.local and you can see that I have the convex URL here. So that's one way we can do it. But keep in mind in here it says it needs to end in dot site not dot cloud. I'm pretty sure all we have to do is just change this to dot site but let's see if there is another way we can find it perhaps by going inside of the settings here And let's see if I click show development credentials, there we go.
HTTP actions URL. This is the one we need. Let me just check. Is it exactly as I thought, convex URL? Yes, it's exactly the same.
It's just not dot cloud, it's dot site. So find this HTTP actions URL and then let's get inside of a clerk. Let's head inside of the dashboard again. And in here let's go inside of configure and let's find the webhooks. In the webhooks here let's go ahead and click add endpoint, let's add it and let's do forward slash clerk dash webhook.
So this is the new endpoint that we're going to create later on. But this is important. HTTPS and then convex.site like this. Now the events that we're going to subscribe to is going to be in my case subscription events like this. In fact the only one that I will listen to is .updated but I just want to bring to your attention that you can manually select all of this if you want to.
So I'm only interested in the subscription.updated event. That's the only one I need. In here I don't think I need anything so I will just click create like this. Great. Now that we have that we need the signing secret right here.
So let's copy the signing secret. That's very important. So click here to reveal it and then copy it entirely. And first things first, let's add it to our packages backend environment.local. So I'm going to add...
Oh, do we already have the... Oh, my apologies. No, this is not the one. Okay. I was searching for what's the correct the name to store this into so clerk webhook secret and Let's add it here so just after clerk secret key and Once you've added it here super important also go inside of your project settings and the environment variables, add and add it here.
Clerk webhook secret and safe. There we go. Now you are ready to actually receive something here. So what we're going to have to do before we can continue is just add a small package. So PNPMF backend add Svicks.
So Clerk uses Svicks to handle their webhooks so we need it here in order to authorize the headers from the webhooks. So make sure you have added that inside of your back end and once you've done that Let's go ahead and let's create our endpoint. So inside of packages backend convex create a new file http.ts. Exactly like this. It's a reserved file.
And now in here let's go ahead and let's create the router. So const HTTP will be HTTP router from convex server like this and make sure that you do export default HTTP. Once you do this I think that there shouldn't be any errors. Let's see. Running TypeScript, finalizing push, there we go.
So convex must have a default export of the router, we just added that so everything is fine now. Great. And now let's add our first route. So similarly to Express or Hono, HTTP.route, path, clerk-webhook, method, post, handler, post handler HTTP action which you can import from generated server asynchronous context request like that and then in here let's go ahead and let's first validate the event and then listen to subscription.updated and then we're going to call our system subscriptions absurd internal mutation so that we either create the new subscription table or update existing. So every time the organization updates, my apologies, upgrades, this webhook is going to fire.
So it's important that this clerk-webhook matches exactly how we name our route here, clerk-webhook. See if I change this to 123, I also have to modify this to 123, okay? But for now just leave it to be normal. So the first thing we're going to have to do here is create a validate request method. In order to do that, let's go ahead and import something from Svicks, which we just recently added to our project.
So import webhook from svix like this and let's also import createClerkClient from clerk backend. And I think let's also add type webhook event from clerk backend. Now let's go ahead and create this validate request function. So I'm going to do that here. Const, actually let's do asynchronous function validate request, it will accept the request which is a type of normal request and it will return back a promise.
The promise will either have a webhook event type or null. So we basically have to validate that whoever is trying to access this clerk webhook endpoint is authorized to access it and the only person, The only thing that's available to access this is Clerk. So we have to make sure that their headers match exactly what needs to be decrypted when decrypted with our Clerk webhook secret. So there are no chances anyone can access this but Clerk who we expect to access this, right? So let's go ahead and do const payload string awaitRequest.text so we turn it into a string.
Now, let's define the SWX headers. Let's start with SWX-ID, request headers.get SWX-ID or empty string. Be super careful here. There are no type safety here so make sure you don't misspell. Svix-id, Svix-id.
Let's copy this and let's change this to SvixTimestamp and SvixTimestamp2. Then let's do SvixSignature and SvixSignature here. So double, triple, quadruple check that you don't have any typos here because it's going to be hard for you to debug why your requests are failing. Now let's attempt to get the webhook by doing new webhook process.environment.clerk.webhook.secret like so. The clerk webhook secret is what we've just added here in the environment local and what you absolutely need to have in here as well.
So we copy that from here, the signing secret. Perfect. Now that we have this, Let's go ahead and do a try and a catch. Let's first do the catch. So console.error error verifying webhook event error Actually let's not do it within the string, let's just add it like this.
And let's return null. And in the try let's do return webhook.verify payload string svx headers as unknown as webhook event like this. So now we have an official way of verifying that whoever is trying to access this is coming from Clerk. So we can now go back instead of our HTTP action, and let's do const event and attempt to get the event using await validate request, which we just wrote and pass in the request. So this will either throw an error or this will either return null or it will allow us to have the event and then we can check if the event type is subscription updated.
So in case there is no event it means either an invalid event has been sent or someone malicious is trying to access this. So we need to treat this as if an unauthorized user is attempting to access an authorized route. So return new response error occurred. And status 400. We don't let anyone past this point.
Otherwise if we do get a valid event that can only happen If it's actually clerk who sent this event so let's switch event.type case subscription.updated because that's the only one we are listening to So constant subscription will be event.data as status string payer which is an object, an optional object which has organization underscore ID. So clerk uses this casing. They don't use the same casing as we do so be careful how do I know that it's payer and how do I know it's this if you go inside of clerk here if you go inside of event catalog search for subscription search for subscription dot updated it will scroll down to here open that go inside of data and in here you can find the payer object. The payer has the organization ID only populated for organization payers. So yes, technically it could be optional.
Due to the nature of our app, we're almost 100% certain that this will exist. But yes, technically it is optional by their type safety definition but you can see how it's typed organization ID and outside of the payer I also know that there is a status which can be abandoned active canceled ended incomplete or past due. So now that I have those two things what I'm going to do first is I'm going to grab the organization ID from subscription. My apologies this is just one subscription. So subscription.payer.organizationid and make sure to put a question mark here.
In case there is no organization ID, I have no idea who purchased this and I don't know what to do with my database. So I have no choice but to return a response with an error missing organization id because I don't know what organization should I update what table should I create I'm missing a reference to the buyer here. Otherwise, first things first, one thing that still doesn't work is that even though I'm upgraded here in this account as you can clearly see in my plans and billing, Let me just refresh. So if I go inside of my plans and billing, obviously I am active on the pro plan, but still if I try to invite someone, it's not working. So Antonio at example.com is still throwing me an error.
So how do we fix that? Well, we fix it by using the clerk backend API to well upgrade. So right here, what I'm going to do is initialize const clerk client create ClerkClient secret key will be process.environment ClerkSecretKey. Now just double check that you have clerk secret key added here. Make sure there are no typos and also make sure that you have it inside of your convex environment variable, clerk secret key.
And you can also add a fallback like this. Perfect. Now that we have the clerk client, we can actually use that to upgrade this organization's ID settings and to increase the number of allowed members. So await clerk client organizations, update organization using the organization ID, maximum allowed memberships and in here you can see that you can set a number to how many members you want to allow. So I'm going to set five members.
So now this will no longer be an error because we will increase this from the limit of 1 to 5 because they are now paying members. And let's await context run mutation internal dot system. I didn't import internal. I didn't import internal so let's do internal from generated API system.subscriptions.upsert passing the organization ID and status to be subscription.status. And yes, let's go ahead and also modify this so const new max allowed memberships if subscription.status maxAllowedMemberships if subscription.status is equal to active it will be 5 otherwise it will be 1.
I think that's the correct logic, right? So if we receive this webhook event, subscription has been updated and we see that subscription status is set to active, then we give them the new number, hey, you can add up to five members. Otherwise, if this becomes canceled or past due, we fall back and give the user only access to one user. I think that makes sense. Great, So let me see what else do we need to do here.
So yes, this is a switch case so we need to properly finish it. So first things first, let's break. After that, let's add a default console log ignored clerk webhook event, event.type so we know if any ignored events are happening here and let's return new response null with a status of 200. This is important. You always have to return something at the end of your webhooks.
Otherwise, they will be considered failures. Great. There we go. If we've done this correctly, this should now all work. But we can only test this in a new organization.
So either switch to a new organization or just create a new one. And let's try again. So I cannot access my knowledge base. I cannot access voice assistant and I cannot access my widget customization. Not only that, but I cannot invite anyone.
So Antonio at example.com, send invitations, not working. Now I'm going to upgrade and Not only I'm going to upgrade, I'm also going to keep a close track here in the activity or maybe logs. Whatever shows first. So you can see, you can select this endpoint and I think in here you can track message attempts. So Let's click subscribe.
Let's click pay with test card. Let's see if it works. Maybe we forgot something. So immediately I'm upgraded. That's great.
So I already know I can now see the knowledge base. I can see the voice assistant, right? But let's see, Clark. Let's go ahead and refresh here. And there we go.
I have a succeeded subscription type in here and you can see exactly that I had the payer, I think somewhere here, I can't find where it is, there we go, payer right here and I can find the organization ID for my payer. Perfect. Which should mean, I'm trying to find, yes, so We have to go inside of here, inside of data, subscriptions. There we go. I have a new subscription with the organization ID and status active.
And I can think I can already check inside of organizations. So it's this one, my test, the previous one, right? If I go inside of settings here, there we go. Limited members are now five because we upgraded them. So if I've done this correctly, and I think I did, if I go ahead and I add a random email here and click send invitations.
There we go. Invitations successfully sent, which means that we officially allow this new member invites. Amazing, amazing job. So since this chapter is already an hour long and we've done the hardest part, I'm going to end it here. And then in the next chapter, we're just going to do a very easy API check if we have the subscription table or not.
And using that, we are going to either allow or block AI features and things like that. But most of the things are finished. And I'm pretty sure some of you already know exactly how you would wrap this up but don't worry we are going to do it together just in the next chapter. And yes one thing I think exists now in the subscriptions if you actually find a subscription like this one for the test. I think that not sure where but I saw that maybe you can now kind of like force.
You can kind of force the end of the subscription. I'm just not sure how. I'll try and research for the next chapter. There we go. So, yes, cancel subscription is one thing, but end subscription kind of simulates if the user stopped paying and their active paid tier expires.
So if I click end subscription now, what should happen is that first of all, I should get an upgrade here. Let's see, I'm not getting an upgrade. Okay, that's not... I'm not sure if I did something incorrectly or maybe that exact event doesn't fire a webhook. But you can see this is upgraded immediately.
I am now blocked once again. But yes, I'm not sure if that fired an event. Let's see inside of where is it? Where are my webhooks? I can't find them.
Here they are, the webhooks. Okay, so subscription updated right here. This is the new one that just happened. So we have the data here. We have the payer, that's correct.
And the status was still initialized as active. Okay, so I think this is just a bug with their development feature. Of course, I think this is just a development feature, which allows us to kind of end subscription now. I just think they didn't, they set the incorrect status in their webhook event. I think in 99% certain in production, when it ends naturally and not via this development button, the status will correctly change to ended or canceled.
Right? So that's why this appears as a bug, but it's actually not. It works as intended. They just forgot to change that status. I will try to double check this for you, but I'm fairly certain that's what happened.
All right, so yes, we can kind of consider this API protection because it was mostly webhook, but sure, I'm satisfied with this for now and then in the next chapter, we'll do the actual API protection. Amazing, so 31 subscriptions, so let's go ahead and merge this. So I'm going to stage all of my changes, 31 subscriptions, commit, I'm going to open a new branch, 31 subscriptions and I'm going to publish the branch. And now let's open a pull request. And here we have the summary.
We added BillingPage with new BillingView and PricingTable. We introduced premium feature overlay prompting upgrades for non-pro users. We enforced ProPlan access for customization, files, and WAPI pages with graceful fallback. We updated account UI primary color for authentication components, So this is referring to the variable color we added to the clerk provider. And in here we have a sequence diagram explaining both how our premium feature overlay works as well as how our new Clerk webhook works with in-depth explanation of how we actually verify the payload.
Amazing. We do have some comments here. Most of them are recommending to not render the files view or the whatever view within the premium feature overlay. And yes, the premium feature overlay can be used as simple as just, you know, a normal tag here. But I think it's cool to kind of give the users a sneak peek of what's behind.
Right. So that's the only reason we are doing that. But I'm not sure Code Rabbit understands what we're trying to do here. So yes it is to prevent unnecessary fetching. True.
Yes. But we will protect that in the back end in any way. So. Great. And in here I don't think it has the information about the newest organizations prop because it is technically in beta.
So that's why it thinks for organizations doesn't exist but it does exist and we need it for our use case. Amazing, amazing job. So once we merge this, let's go ahead and change back to main branch and click synchronize changes. This way we will have up to date with our remote branch which we just merged into. And as always I like to finish by confirming that I have that merged right here.
Perfect. I believe that marks the end of this chapter. Amazing, amazing job and see you in the next one. Thank you.