In this chapter, we're going to integrate payments and subscriptions to our application. And we're going to do that using Polr, an open source payment processor. So why am I choosing Polr over all other methods? Looking at their website, you can find exactly what Polr is and how it works. Polr is an open-source billing infrastructure platform designed specifically for developers who want to monetize their software without the complexity of traditional payment systems.
Polar is also a merchant of record, which basically means that you can leave the billing infrastructure and international tax headaches to them and just focus on growing your business instead. They handle VAT, GST and sales tax in all jurisdictions. They also do proper EU VAT handling. And they also do automatic tax calculation for you. This is not everyone's cup of tea.
Some people prefer using Stripe and when they met the threshold for tax, they have their accountant handle that. That's a perfectly fine decision to do so. In my case, I have a company registered within EU and it is very important for me to have a merchant of record. But if the first two reasons aren't enough for you, there is also a third reason, developer experience. I am not exaggerating when I say that for the time it takes me to integrate Stripe in just one project, I can integrate Polar in 50 projects.
And I am not exaggerating. You're going to see that in a second when we actually start developing here. So Polar was developed with developer ergonomics in mind. They put the developer experience in the front seat and they're not lying when they say that. You will see how incredibly easy it is to integrate Polar within any adapter that you need, be that Next.js, TypeScript or something even more specific like BetterAuth which is our use case.
And on top of all of that Polar is completely open source which I've already mentioned in the beginning and if you're still having doubt about their reliability given that they are a new player in this payment processing world, I can tell you that I have personally been using Polar over half a year for all of my sales and I have not once had a bad experience. And this is also true for their support team who is extremely responsive and here for any questions that you might have and same goes for the amazing developer community that they have created within their discord. And here's some other information. Polar is the cheapest merchant of record on the market and no hidden fees, which is unfortunately a practice for a lot of merchant of record providers and I'm very happy that Polar does not do that. You can very clearly see exactly how much money goes into your bank account after all the tax has been handled.
So now let's go ahead and use the link you can see on the screen to actually visit their landing page here. So you can turn your software into a business with six lines of code and they are not joking when they say that. And you automatically get subscriptions, benefits, checkout links, usage billing, customer portal, metrics, and even more than that. When they say integrate under a minute, again, they are not joking. The only reason this chapter is not one minute long is because I'm taking time to explain what I'm doing.
So in here they've listed some of the more popular adapters like Next.js adapter and you can see how easy it is to add a checkout route here, but they also have some very specific adapters like BetterAuth, which is absolutely amazing because that's exactly what we need. And in here you can already see something interesting. Inside of the BetterAuth config, they have this field called create customer on signup set to true. This one line is so useful that it will save us so much headache and so much time from making sure that we can easily check whether any user in our database is a pro tier or a free tier customer. Let's go ahead and just try and integrate Polar so you can see exactly what I'm talking about.
So go ahead and log in. I'm going to be using Google here. Now depending on your state here you will either have a project or you will have a new organization screen which you can access manually by going on new organization here. Now we're going to have to create two organizations here So I'm going to call this first one node base and I will call it production and organization slug needs to be unique. So I would suggest adding some unique identifier here.
I will add CWA as in code with Antonio. So this will be taken for you, so make sure you add something else. In here you have supported use cases and prohibited use cases if you're interested for that for your own custom app later. Make sure you understand those terms and click Create. And after you have created an organization you will be prompted to create your first product.
I'm going to call this NodeBase Pro and the subscription will be unlock the full benefits of NodeBase. You can add some subscription image if you have it, otherwise you can go into pricing, which can be a one-time purchase, monthly or yearly. And I'm going to set this to be $29.99. For the automated benefits, you can offer a discord invite, a github repository invite, file download, license keys, a lot of things and they will automatically be revoking if the subscription has failed. And let's just click create a product.
And once the product has been created in here, you will be asked to integrate checkout. But I would actually suggest that you just click go to dashboard. So What you're looking at right now, at least at the time of me making this video, is you are in production now, right? So you can't exactly test this product. I just wanted you to have your node-based production organization here, but you don't really need to get the you can find the developer token here later when you need it in production but for development you actually need to click go to sandbox and once you are in sandbox you will see a big screen here changes you make here don't affect your live account payments are not processed and in here you have to create a new organization once more.
So let's go ahead and call this one node-based development and again I'm going to add an original slug here. Make sure you click. I understand the restrictions and create this new organization. And now you will have both your development and your production organization. So let's go ahead and call this node base pro unlock the full benefits of node base.
Let's go ahead and set the monthly price to be $29.99 and let's click create product. And now we can actually follow the checkout instructions here. So I'm going to go ahead and select BetterAuth here and then I'm going to follow the install dependencies script. So we have to install BetterAuth which we already have so no need for that. We can just focus on the other two packages here.
So npm install polar-sh-better-alf and polar-sh-sdk. Make sure you install those two packages within your project. And then go ahead and create a new environment variable. I'm going to call this app development and it will never expire. And I will select all scopes simply so it's easy to develop with this.
And let's click create. Go ahead and copy this immediately and let's go ahead and you should now have two uncommitted changes here. So basically polar sh better out and polar sh sdk. Now let's go inside of our dot environment file here and let's add polar. So I'm going to add polar access token and I'm going to store it right here.
So this is obviously for development. Great. And now that we have that looks like we also need to add the polar success URL. So let's quickly add it here. Polar success URL is just going to be our app.
So I don't think. Yeah. Basically exactly the same as better out the URL right here. Perfect. We have that ready.
And now let's go ahead and integrate the checkout. So what we're going to do now is we're going to go instead of auth.ts instead of source lib where we have the Prisma adapter and the email and password. So Let me show you how that looks like here in the files structure instead of the lib folder auth.ts. And inside of here let's go ahead and let's import checkout, polar and portal from polarsh betterauth. And now let's go ahead instead of the lib folder and let's create a new file called polar.ts.
In here let's go ahead and import polar from polar sh sdk and export const polar client new polar access token process dot environment and let's go ahead and add polar access token and make sure to add server sandbox. I'm going to add a comment to do change in production. So just double check that you have correctly named the polar axis token here that you don't have any typos. Otherwise, it will not work. And yes, you can also create some kind of logic here.
For example, I think in the database.ts we do that. If process.environment.nodeEnvironment is not production. In that case use sandbox otherwise production like this and then you don't have to do it manually But you might have to modify this a little bit further if you will have staging or if you will have like some development previews, right, which are deployed because then it will use the production server. So yeah, I'm not sure if that's what you want, but the simplest way is this, but maybe a more reliable way would be older server like this. And then depending on where you deploy you would have to modify it so Let's go ahead and just just for now.
Let's leave it at sandbox. I think it's easier So polar client sandbox, let's go inside of database. My apologies inside of our out dot TS here and Now let's import polar client. Not from here. But from here polar.
And then let's simply go ahead and add plugins in here open polar, set the client to be polar client create customer on sign up, set to true use is going to be an array checkout and portal in checkout let's go ahead and add products open an array here and open an object and add product ID and add slug. Now let's find our product ID and let's choose the slug. The slug can be whatever we want so I will set this to be pro and I think I can actually find you can see they pre-fill the product ID for you so you can actually copy it from here but just in case you don't want that so I'm going to add it here now but just in case you closed this screen or cannot find it. You can go to dashboard, go inside of your products and click copy product ID. And then you will be able to find it here.
And then let's also add inside of checkout success URL process dot environment polar success URL and authenticated users only set to true this way your customers can only be users who have an account using BetterAlf. So in order to test if we did this correctly, we already added portal, all right, I just wanted to make sure that's true. In order to test this, let's do the following. We don't have to add anything new to our Prisma schema. Instead, let's do npx prisma migrate reset.
So let's remove all users from our database now. So all data will be lost. Yes, the reason we are doing this is because we just introduced create customer on sign up field. So I'm going to do npm run dev all. I'm going to go to localhost 3000 here.
And I want to keep your poller open and click inside of customers here. You can see that I have no customers right now. But the moment I created this one, so let me go ahead and click sign up this time. AntonioMail.com 12345678 12345678 and when I click sign up nothing will change for the user really so let's just sign up, here we are but on Polr now when you refresh you now see your new customer here. So that's what happened when you click on AntonioMail.com here and when you scroll down here you can see something called external ID.
This external ID is a one-to-one mapping of what's currently stored inside of your database. So if I go ahead and close this and open my users, let me just refresh this So I can load the user. You can see the ID starts with capital T then lowercase D. And oops I thought I was here. My apologies.
You can see that is exactly the ID that's been stored here in the external ID column so that's right, your customers are now automatically created you don't need a webhook, you don't need any after signup event All of this happens automatically and very reliably for you. And the rest is now extremely easy. So let me show you. So you probably wondering, all right. So now when the user clicks upgrade to pro here, how do we upgrade?
You will be amazed by how simple this is. Let's go instead of app dash sidebar to develop that. And we don't even need to add any new imports because we already have the ALF client. All we have to do is find the upgrade button and change the onclick to call altclient.checkout and select the slug to be pro. But of course I missed one thing which you can immediately see by the lack of type definition here.
Let's quickly go inside of auth client. It's located in the lib folder auth client. I forgot that we have to add the plugins here as well. So just polar client from polar better out Like this and now you can see immediately the error the type error has gone Why slug Pro Because that's exactly what we have defined here. The slug is called Pro.
So we can now easily refer to it without knowing the ID of the product. And now, when you click Upgrade to Pro, wait a few seconds and you will get redirected to the payment screen. If you keep seeing the white screen, you can just hit refresh and it will then load NodeBase Pro. This can happen when you are logged in in your Polar account and you try to purchase your own product. It's kind of like a protection in some sense.
And in here if you want to test it out you can very simply just use Stripes test cards which are these ones. The expiration needs to be in the future. The security code can be whatever you want. And in here you have to follow the guide as you would normally purchase. So I'm in Croatia, so that's what I would choose.
And you can see the taxes that are going to apply, that are going to be applied. And once I click subscribe here I will simply be redirected back to my product here. Now nothing is really happening for me here. But if you take a look at here you can see the actual revenue that has just happened for this user. You can see that now I am on $29 of monthly recurring revenue.
And how do we now use that information? How do we now give this user some special access? Well, very easily. So let's go ahead, for example, and let's hide this sidebar menu item if the user has already purchased or subscribed to Pro. Now this can also be done in many ways but the way I'm going to do that is by going inside of my features folder.
So instead of source features I will create a new folder called subscriptions so features, subscriptions feature or payments, whatever you prefer and I will create a hooks folder inside and then I'm going to create use-subscription.ts let's go ahead and import useQuery from 10-stack react query let's import outClient from lib outClient Let's export const use subscription to be returned use query, query key, subscription, query function, asynchronous method and let's destructure the data from await out client customer dot state. So you can see how easy it is to get the customer's state. Since we are connecting our internal database ID with Polar's external database ID, I mean it's transferred to Polar as external database ID, we can very simply get the Polar state by calling whatever is the current out client session dot customer dot state. You don't have to pass this inside of React query. This is completely optional.
I simply just prefer using use query whenever I can. You could have very easily just done that here somewhere, but I personally like it this way much more. And then let's export const use has active subscription hook. So we can very simply use this throughout our client side. On the back end it's much easier than this because we can very simply just await without needing to refresh or something.
But in here we kind of have to rely on some refreshes and refetches, right? So let's go ahead and get the data in the remap it to customer state. Let's get is loading and the rest of the values all from use subscription. And then let's define has active subscription as customer state question mark active subscriptions and customer state dot active subscriptions dot length a larger than zero because we only allow the user to have one active subscription at the time which makes it very easy for us to confirm whether this user should see their pro status or not. If you had a more complex situation you would have to look through the active subscriptions to find the one with the id that you need.
And then in here I'm just going to return has active subscription. The active subscription will be customer state question mark active subscriptions question mark first in the array I will pass is loading and I will pass the rest as well. There we go. So we now have use has active subscription. So I'm going to go here in the app sidebar and I will add use has active subscription.
Let me show you the import here from features subscriptions hooks use subscription. Has active subscription and is loading will be extracted from here. And now let's finally go down here where we have the sidebar menu item. And let's check if not has active subscription. And if we are not loading the state of the user's subscription, then show this upgrade button.
So now if you take a look, you will see the button doesn't exist because my Antonio user is premium. But if I sign out and if I sign up again with a new account I'm using the same password here so let me go ahead and sign up. This new user will now see the subscription here. Maybe I need to do a refresh. Here it is, yes.
I need to do a refresh and the reason it stayed invisible is because the previous user was premium and when I signed out we didn't clear the react query cache. Now you don't have to really worry about that because again this isn't our layer of protection. This is the same as redirecting the user away when they are logged out. This is just to improve user experience because the true premium protection will be inside of our data access layer by introducing the same way we introduced protected procedure in TRPC, we are going to introduce premium procedure in TRPC. So you don't have to worry about glitches like this in a sense that obviously it's not good to have any bugs, but in a sense that this is a security leak, it is not.
So don't worry, just because you forget to clean up some state it won't mean that someone will be able to use pro features because pro features will be restricted using TRPC data access layer protection. So just refresh and then you should be able to see upgrade to pro here. And you can also now enable portal down here. So billing portal should always be active if you ask me. So customer and then just call portal.
It doesn't matter if the user is pro or not when you click on billing portal it will simply redirect you to your customer portal which will say no active subscriptions no benefits available right super simple but if you sign out And if you go inside of Antonio mail.com or whatever you added as your premium customer. In that case in here when I click billing portal You will see that I have a current active subscription and I will be able to cancel it from there as well. So here it is. $29.99 per month. I can change plan.
I can view subscription. I can do what I want. Perfect. So now let's go ahead and let me just show you one more cool thing. This is actually very new.
What they've added is the ability to trial. So let me expand my screen here. Let me collapse this internal sidebar that they have. You can actually select a trial period if you want to. So configure a free trial period for this product.
So if you enable this you can select for example one month of free trial or one day one week right. Whatever combination you want and just go ahead and click save product. And what's going to happen now is that if you go back to your app here. And let me go ahead inside of sign up my apologies inside of the other account. So this one one two three four five six seven eight.
Let's see if I entered that correctly I did. Let me just refresh. OK. You can see the beers and when I click upgrade to pro here you will see that it says one month trial, which means that I will not be billed. And if I take a look at my customer portal later, it will say status trialing.
So it is that easy to add a free trial to your product as well. Excellent. So I purposely want to leave one of my customers to be free so I can demonstrate creating a protected premium procedure. In order to do that let's go inside of init.ts within source.drpc folder. In here we have already created protected procedure so now let's extend it.
Export const premium procedure will be protected procedure dot use. Let's go ahead and make this an asynchronous method and let's destructure context and next. Let's get the customer by doing await polar client which we can import from lib polar so just make sure where is it here it is from lib polar so polar client dot customers dot get state external and use the external id context out user id so that's why we are extending the protected procedure so that we have the out session here so that we can obtain the user ID which is mapped as external ID in polar and then in here let's go ahead and check if not customer.active subscriptions or if customer active subscriptions.length is equal to zero let's go ahead and throw new TRPC error here with a code forbidden and a message active subscription required. Otherwise, let's return next. And let's go ahead and pass context here, spread the existing context and simply add customer inside too in case we want to do something with the customer object.
And it is that simple to integrate a premium procedure. So let's go ahead now and test it out quickly. So I'm going to go inside of let's see actually we'll test it out in some other example in the next chapter simply because we now have to convert something to a client page. I think it's unnecessarily complicated for something that we can very easily come back and test. But I think you already understand how it works, right?
It works the same way as protected procedure, but now it's a premium procedure. So instead of your app, underscore app, in source TRPC routers, if you wanted to change test AI, you can change it to be a premium procedure, right? Instead of a protected procedure. And then this will throw an error if the currently logged in user isn't subscribed. Let me see if I can very quickly test that somewhere.
Inside of source app, dashboard, rest, Let's go ahead and create a subscription. And let's go ahead and do a simple page.tsx, use client. Let's go ahead and let's import use DRPC and let me add use mutation here from 10 stack react query export. Let's do const page. Const drpc use drpc const test-ai will be use mutation drpc test-ai mutation options.
Let me fix the typo use mutation. Let's return a button from components UI button. Click to test subscription. And inside of here, let's add on success toast from sonar dot success, success. Otherwise on error.
Success toast from sonar.success success otherwise on error toast.error failed. This way we will see what's going on. And let's add onclick here. Testai.mutate And in fact maybe we can grab message from here. And then show the message.
Like that. And let's export default page. So now head to http://localhost3000.com slash subscription, just one. Like this. And in here you should see click to test subscription so I'm currently on a free user and if I click this let's see what happens active subscription required I get an error But if I go and sign out.
1, 2, 3, 4, 5, 6, 7, 8 and I log in here and let me just refresh and go back to subscription and I click click to test subscription I get back success. Now you might get an error here if you don't have ingest running so just make sure you have ingest running too and now AI is running in the background. Amazing. So we successfully wrapped that up so we can now remove the subscription page here because we don't need it. And I think that we developed everything we need for now.
So the only thing that I kind of don't like is this use subscription, which is being cached through logout. So the new user who logs in temporarily gets the premium state of the premium user. I will look into the best way of fixing this, but again, that's just a UI bug because our actual protection is inside of premium procedure here, which will always make a separate API call to Polar to confirm whether this user that's currently logged in has the active subscription or not. And it is simply extending the protected procedure here. So this cannot really be hijacked by some fake ID.
We are using the ID from the session that we have. So I believe that marks the end of this chapter. And I hope you realized how fun it is to use Polr really. In order to develop all of this, it would take me so much time and headache with Stripe and not to mention the webhook and then the local testing of the webhook. It's just, you know, a headache.
But with Polr, it is just ridiculously easy to do so. So let's see, we have set up Polar, we integrated with BetterAuth, we created checkout and we created the billing portal. And now let's go ahead and commit that. So, chapter 10 payments, I have 9 files here, you can see them right here. You might have 10 files if you accidentally have some error for mprox because it can create like an additional file but these are the files you should have so I will click down here on main I will click create branch 10 payments I'm going to then stage all of my changes and I'm going to do 10 payments and commit and then I'm going to publish the branch and after I publish my branch as always I'm going to create a pull request so I can review my changes and see if we need to fix any critical mistakes.
And here we have the CodeRabbit summary. Subscription Aware sidebar. Upgrade to Pro appears only when applicable and launches checkout screen. We added in-app billing portal for managing subscriptions. Premium actions now require an active subscriptions.
Non-subscribed users see a clear error and an upgrade path. We also added the dependencies to support said checkout, billing, and subscription state. In here as always, file by file walkthrough and in here we have the sequence diagram. So this one is explaining how our upgrade to pro works which very simply uses ALT clients polar plugin to call the checkout function which simply creates the checkout session and redirects your user to that URL. The same way customer portal works.
And in here we have a definition of our premium procedure. So when the user calls mutate test AI we invoke a premium procedure which again uses the polar climb to get the customer state using user id. If the user has an active subscription we proceed with whatever that procedure is of wrapping otherwise no active subscription with throw an error active subscription required. In here we do have a few comments but it's mostly the things we already knew. My upgrade typo and in here a guard against missing polar success URL.
So if someone forgets to pass that we should either throw an error or we should add a fallback. So we are going to look into that here. Same thing here what I tried to do dynamic polar server variable I will see into in the next chapters how we can improve that and looks like that is it so let's go ahead and merge this pull request and once it is merged we can go back to the main branch here as well and we can click on synchronize changes and ok and after that head inside of your source control graph and just confirm that you just did 10, you detached and you merged back in and that means everything is as it should be and now let's go ahead and wrap up the chapter we pushed to github we created a new branch a new PR and we reviewed and merged. Amazing, amazing job and see you in the next chapter.