{
  "video_id": "Kh2FjTFzxJc",
  "transcript": "Imagine jumping into a video call, and the other side isn't a real person. Instead, it's an AI agent built to help, guide, or answer your questions in real time. That's what Meet AI is all about. You're watching part 2, the final chapter of this full tutorial. In part 1, we set up authentication with BetterAuth, connected our database using Neon and Drizzle ORM, Built real-time AI video calls with Stream and powered background jobs using Ingest and OpenAI. We built a custom agent system where users can create AI personas with their own instructions, ready to join video calls. Then we built out the full meeting flow, from scheduling to joining calls, and made sure the entire app works seamlessly across devices. We also followed a full Git workflow throughout, with all requests automatically reviewed by CodeRabbit AI. Now, in part 2, we're finishing the experience. We'll add searchable transcripts, AI chat that understands the full meeting, implements subscriptions and free trials with Polar, and finally walk through the full deployment process. And now, without further ado, let's get started. A good friend of mine runs a newsletter called Almost Done. It's packed with sharp, practical insights for developers. Short, thoughtful, and genuinely worth your time. Check it out using the link below. In this chapter, we're going to add transcript and chat to our completed state. So let's start by adding the get transcript procedure. Confirm that you are on your main default branch. And let's go inside of our meetings procedures. So right here at the top of the page, I'm going to add a new get transcript procedure. So this procedure will be a protected procedure which accepts the ID of the meeting we are trying to get the transcript from. Now inside of here, let's go ahead and let's get the existing meeting using the input ID and ensuring that the currently logged in user from the context has access for this meeting. In case we don't have access to the meeting, let's throw an error, meeting not found. Now in case there is no transcript URL, let's just return an empty array. And now what we have to do is we have to import jsonl from jsonl parse stringify. Now let's go down here and let's actually parse our transcript from the URL. So our transcript is await fetch existing meeting transcript URL. We return it into text and then we use jsonl parse and let's import our type stream transcription item, which we defined in our ingest job. Otherwise, if there are any errors, such as this no longer being available, we will just return an empty array. Now from here, let's populate the speakers by creating a set of unique speaker IDs. After that, let's go ahead and let's create the user speakers, which means we have to import the user from database schema and in array from drizzle orm. Make sure you have added both of them. So in here I have user from database schema. We also use the generate avatar URI in case we cannot find the user image, but we already use it in generate token, so that's nothing new. Now, after we add user speakers, let's do the same thing to add agent speakers. But this time we are searching from agents, in array agents ID, and we are using bots neutral. We already have the agents imported, but make sure that you have the agents from database schema. So now we have the user speakers and the agent speakers. So what we can do is we can combine all of them in a one single array called speakers. And then let's go ahead and let's open transcript with speakers constant. So transcript with speakers, transcript.map and then individual item here will be used to find a speaker using the speaker ID and item speaker underscore ID. In case we cannot find a speaker, we will do the same thing we did in the ingest. So if there is no speaker, we return this existing item here and we are simply adding the user with name unknown and an image unknown like that. But in a case that we do have a speaker, we are simply gonna go ahead and return the item with that user right here. And then finally, we can just return the transcript with speakers for this procedure. Now let's build the transcript component. In order to do that, we first have to install React Highlight Words. Once you have installed it, let's go ahead inside of our completed state here and let's find our tabs content here. And let's just add a new tabs content with the value of transcript. And inside of here, add transcript with a meeting ID of data meeting or data ID. Now let's go ahead inside of the components and let's create transcript.esx. And in here, let's go ahead and let's import use state, format and search icon from Lucid React. Let's import highlighter from React highlight words. And looks like we also need to import the types for this. So let's go ahead and do npm install the types React highlight words. Und jetzt sollte man noch mehr haben die Error hier. Dann, wir import den Use Query von TanStack React Query. Wir import den Use TRPC von TRPC Client. Wir import den Input. Wir import den Scroll Area. Und wir import den Avatar und Avatar Image. Und wir können auch import den Generator Uri von LibAvatar. Now that we have all the imports, let's create the props with a single meeting ID inside. And let's go ahead and assign the props to our transcript component. In here, let's prepare our trpc. And then let's go ahead and structure the data from use query and pass in trpc.meetings.getTranscript. And let's do query options in here as well. And pass in ID, meeting ID. Like that. So now we have our transcript populated with users inside. Now let's add our search query here, like that. And let's add our filtered data here. The only thing we'll have to look out for is for the fact that data could be an empty array. So let's just do this. And now let's build the actual component here. So let's return and let's add a div with our background and our padding. So background white, rounded large, border, some padding, flex, flex column, and some gap between the items inside. Let's give it a paragraph with text small and font medium of transcript. And then in here, we're going to add our input with the search icon. So this is what this looks like. A div with class name relative, our input component with placeholder search transcript, class name with relevant padding and a maximum width, value using the search query from above, on change, changing that search query, and a search icon with an absolute positioning because it's inside of a relative container with left to positioning. Das ist das wir, dass wir das Padding leften und Top, in der Mitte der Y-Axis haben. Same mit der Translate. Wir geben es eine Size und Text Muted Foreground. So, wir gehen in der Completed State und wir import den Transcript, weil das die beste Weg zu sehen, wie es sieht. So, wenn ich auf die Transcript klick, Sie sehen, dass Input. Now, below den Input, wir gehen und wir adden eine Scroll Area, outside of this div. So we add a scroll area here and let's add a div with some minimal spacing here. Inside of this div, let's go ahead and let's actually iterate over our filtered data. So we filter the data using the search query. Now inside of this filtered data, let's go ahead and let's return the following. I want to return a div with a key of item start timestamp and class name of flex flex call gap y2 hover bg muted padding 4 rounded md and border so let's just close that div here now inside of here i'm going to open another div with vertical spacing here so flex gap x2 and center and it looks like i have some issues here. So let's just see what am I doing wrong here. I think I'm missing one more curly bracket here. So don't do the same mistake as I did. Now inside of here, let's go ahead and let's render the avatar component with class name size six. And inside the avatar image, which is a self-closing component without user avatar. and let's give it a source of item.user.image or generate avatar URI and pass in seed to be item user name like that and let's go ahead and pass in the variant initials. Let me just fix this. There we go. Perfect. So now we can already see the transcript beginning to form. Now, after the avatar here, go ahead and add a paragraph rendering the user actual name like this. And let's go ahead and give it a class name of text small and font medium. And below this, let's go ahead and let's add a paragraph, which will simply format using date FNS, right? It will simply format items start timestamp in a proper format. And this is actually important. So make sure you add new data like this. And let's give this a class name of text small, text blue 500 and font medium. So now you will see the actual timestamp, right? And let's just save this and let's just refresh for this to have the proper color. There we go. Und zu Ende, nach dem Paragraphen, nach dem DIV, ein Highlighter-Component, das wir in der Beginn des Chapter installieren. Die folgende Highlight-Classname. Search words for the search query that the user enters and filters by. Auto-escape true and text to highlight item.text. Und jetzt, für Eben, ich kann für Mathen und es will highlight Mathen. I can search for plus and it will highlight plus. And I can search for the number two and it will both filter and highlight. Now it's time to build the Ask AI chat functionality. In order to do that, let's start by adding npm install stream-chat. Once you've done that, let's create the lib for that. So go inside of source lib and let's create stream-chat.ts and go ahead and import server only stream chat and then stream chat get instance and these two variables. Now let's go ahead and let's prepare these variables here. So in here I already have these two and now I will just add the new ones here regarding stream. and the thing is they are exactly the same so if you go inside of your stream chat messaging overview app access key you can see the web hook is the same for chat messaging as it is for video and audio so let's copy the key here anyway next public stream chat api key and the secret is the same there we go i just like to keep them semantically in different variables Now that we have our stream chat and you have confirmed the variables don't have a typo, let's go inside of our meetings procedures and let's go to the top here. And I'm just going to add generate chat token here. And that will be a protected procedure like this. Protected procedure mutation. and we're just going to import stream chat from lib stream chat and create the token based on context alf user id and we are then going to upsert the user and give it a role of admin and simply return that token back so a very simple generate chat token procedure similar to our existing general generate token procedure but for the video there it is it's actually this one is even shorter And just make sure you're using your new Stream Chat library here. Now, let's go inside of the Meetings UI components here and create chat-provider.tsx component. Let's go ahead and let's import Use Client, Auth Client, and Loading State. And create an interface, props, Meeting ID, and Meeting Name. Export the constant here, chat provider. And the first thing you're going to do is you are going to get the currently logged in user here using out client use session. In case we are pending, we're going to return the loading state, basically telling them that we are loading the chat. And then in here, we are going to return the chat UI, which we don't yet have. So just return chat UI with the following props and we are now going to create the chat UI component. Now let's go inside of components and create chat UI dot t s x. And let's go ahead and let's import use state and use effect. Use mutation and channel as stream channel from stream chat. And then let's go ahead and let's install a package called a stream chat react. So NPM install, Stream Chat React. Let's wait a second. And once it is installed, let's go ahead and let's import everything here. Use create chat client, chat, channel, message input, message list, thread, and window. Then you can also add use trpc, loading state, And finally, the styles from Stream Chat React. Now let's create the props for this component, which we already saw right here. So it's going to be all of those. Meeting ID, Meeting Name, User ID, User Name, and User Image. Now let's go ahead and let's prepare the component. So ChatUI, destructuring all of those props above. Inside of the component here, let's prepare the TRPC. And then let's destructure mutate async to be generate chat token here, use mutation and pass in TRPC meetings, generate chat token and simply pass the mutation options here like that. Then let's go ahead and let's create the state channel set channel with a type of stream channel, which we imported here channel as stream channel. And then let's go ahead and let's actually set up our client. So the client will do use create chat client and it will use our new environment key. Next public stream chat API key. Make sure there are no typos. And let's use the generate chat token function right here. Let's just fix this by changing this to be string for undefined. There we go. That seems to fix it here. Great. And now that we have the client, let's go ahead and let's create a use effect, which will actually connect us to the chat. So use effect, which checks if we have this client, and if we don't, we return. And then inside of here, let's do channel client channel messaging, message ID, name meeting name and members user ID, and call set channel to be channel because we defined it right here. And it looks like name is not accepted. So I will just remove it. And it looks like it's working without the name. We don't actually need the name. I just assumed that we will need that field. But it looks like everything is fine like this. So just go ahead and add this properties right here. And then if there is no client initialized, we're going to return the loading state with the title loading chat. There we go. So quite simple, even simpler than our video call initialization here. And now let's actually return the chat interface. So let's start with a div, which will hold background white, rounded large, border, and overflow hidden. And then in here, let's add the chat with client being client, the channel with channel being channel, the window. And then let's go ahead and encapsulate our message list instead of a div, which has flex one, overflow auto, and maximum height defined here using this calculation and border bottom. And below this, let's go ahead and let's add message input. And let's also add a thread down here. Now let's go ahead and import chat UI from .forward slash chat UI. Now in here we have this error here. So let's just do this. There we go. User images string undefined. So if there's no image, we'll just pass an empty string here. And now let's go inside of our meeting ID view, instead of the completed state here. And now we have to add tab content here for chat value. And let's go ahead and add the chat provider inside. So import chat provider from .forward slash chat provider. Make sure you use our component here and passing the meeting ID and the meeting name. So now if you try clicking on here, it should be able to load the chat. Now you can try chatting here, but no one is going to answer. So that's what we have to develop now. First of all, let's go ahead and let's add npm install open AI. So we will be using this package directly. Once you have it installed, go back inside of our webhook. And let's go ahead and add some new imports here. So we are going to add OpenAI from OpenAI. And we are going to add chat completion message param from OpenAI resources index.mjs. It's basically just a type. Now let's add message new event to our list of stream IO node SDK events here. And let's also import generate avatar URI from lib avatar. Now let's go ahead and let's create the new open AI client using new open AI. Now that we have this, I think you don't need to define the key inside at all. I think by default the API key will be the one that we have defined, which is OpenAI API key. But if you want to, you can explicitly add it like this. Now let's go down here to our last event, this one, call recording ready. And let's add else if event type is message.new. So just make sure that inside of your stream chat console, in chat messaging overview, You are looking for that here. So just make sure that you are listening to these things. It's message, there we go. Great. So now inside of here, let's get our event. And now let's destructure everything we can from this event. User ID, channel ID, and text. Now in case any of the three are missing, we are going to return an error because we need all of them to generate what we need. Let's start by trying to get the existing meeting. So existing meeting from meetings where we have the matching channel ID because that's technically the same thing, channel ID and meeting ID and where the status is completed. As always, if we can't find that, let's immediately throw an error. And then what we need to do is we need to find an existing agent using this existing meeting agent ID. And same thing is true. If we can't find that agent, we can't proceed forward. So let's just break this webhook. Now, let's check if the message that this user sent is not an agent, right? So this basically means if someone other than an agent sent a message in the chat, we have to go ahead and respond. And we can only do that with some instructions. So I created a system prompt here in my public assets. My apologies, not the system prompt. Chat instructions. So go ahead and copy that and just paste it here. So instructions use the existing meeting summary and existing agent instructions to know how to behave and how to respond. It doesn't make sense to watch me write all of that. And now below that, let's add stream chat, which you can import from lib stream chat. So just make sure you have added this import here. And let's watch the channel. And then in here, let's get the previous five messages. So we can do that by adding channel state messages and slice by minus five. Filter the message by text and trim the text. So we are only filtering, we are only adding the messages which actually have some content inside, right? And then we are using the chat completion message param here from OpenAI resources. And we are assigning the role to be message user ID being equal to existing agent ID. In that case, the assistant sent that, otherwise the user sent it. And in here a message text with a fallback. And now we can finally generate the GPT response. So GPT response uses our OpenAI client chat completions API. And it adds the initial instructions to the system here, which we defined in this very big variable here. Then spreads the last five messages. So it has some context with an idea of who said what, whether that was an assistant or an user. And then the user's newest message. And it uses this model here. And then finally, we can get the GPT response text here. From GPT response choices first in the array message dot content. In case we are unable to extract the text from here. So if this wasn't found, we're just going to return an error. And then let's go ahead and let's assign the bot avatar using existing agent name and generate avatar URI. Let's use Stream Chat to update that user. So Upsert user, existing agent, existing agent name, and avatar URL. And finally, let's send the message. So channel, send message, text is the new GPT response text, and the user that sent it is right here. So make sure you have your webhack running. Make sure you have npm run dev running. And if we've done everything correctly, I will refresh this. And for example, let's go inside of the summary and let's ask, what did John Doe ask? And let's see if we are going to get a response or not. John Doe asked, what's one plus one during the meeting? Exactly. Amazing, amazing job. You just implemented every single thing here in the summary. So I'm going to go ahead now and I'm going to directly commit these changes here. Actually, no, I'm going to create a new branch. So let's go ahead and create a branch. 26, transcript and chat like this. Let's go ahead and stage all changes. Let's add a commit message. 26 transcript and chat And let commit and let publish the branch And what I going to do just to save some time since we are at the end of the tutorial I going to immediately merge this So let's go ahead and just immediately merge the pull request. And once we have merged it, we can go back inside of main and we can click on synchronize changes, okay. And any second now in the graph, we should see our new pull request being merged right here. Amazing, amazing job. And see you in the next chapter. In this chapter, we're going to go ahead and implement payments. So let's go ahead and visit Polar.sh so we can create our account. There are two websites we're going to use, Polar.sh for production and Sandbox.Polar.sh for development. But nevertheless, start with Polar.sh and go ahead and log in and simply continue with your preferred provider. If it's your first account, you will most likely have to go through an onboarding process where you will just name your organization and maybe add a product. After you've done that, you will see the name of your organization right here. This is your production, right? This is where you will actually pay out. What I want you to do is I want you to go down here and click go to Sandbox. This will redirect you to Sandbox.Polar.sh, right? This development instance. And I want you to repeat the process here. So you will have to create one more account here because these are completely separate instances, which is a good thing, right? And in here, you will see a big banner which says changes you make here don't affect your live account and exit Sandbox. So you are always aware that this is just for development. Now let's go inside of settings here. And let's go ahead inside down here, my apologies, and find developers and click new token. I'm going to call this meet, I'm going to call this app development. And I will set it to never expire and I will select all scopes and I will click create. And then I will simply copy that token. Then I'm going to go inside of my dot environment. I will make sure I'm on the default branch here. And I will add polar access token. Like that. After I have added the token, I am ready to integrate polar with my better out integration. So I'm going to go inside of polar documentation. And I will go ahead and go inside of integrate framework adapters. And I will find better out. In here we have the installation script, but since I already have better out, I'm just going to install these two. So let's go ahead and do npm install and let's add this just in case they fail to install. Great. Once we have this installed, let's go ahead inside of our app, my apologies, inside of lib alf. And in here, what we're going to do is we're going to add our Polar instance. So inside of lib here, also make sure to create the Polar.ts instance. And we can now import Polar from Polar SH SDK. And we can export Polar Client. And make sure you're using your new environment variable here, Polar Access Token. And make sure you put the server to be a type of sandbox. Otherwise, this access token will not work. And as always, let me just show you my Polar versions here. So this is my Better Out plugin version and the Polar SH version. Now that we have the Polar client here, I'm going to go back inside of my Out util. And I will now import the following from Polar SH Better Out. Polar, Checkout, and Portal. And I will also import the actual Polar client from my newly created Polar. And now I'm going to go ahead and I will add some plugins. So let me add the plugins field. And inside of here, I will add Polar and execute it and open an object inside. Inside of the Polar plugin, I will add client to be Polar client. I will add create customer on sign up to be true. And I will add use, which will allow me to add even more plugins here. So let's add checkout. And inside of here, let's restrict the checkout only for authenticated users. And when we successfully buy something, we will redirect to forward slash upgrade. And let's also add portal. So we are using a very nice modular way, similar to the actual Better Out plugin, to add and enable some things from Polar. So this is probably the most important line in this entire setup because this will allow us to synchronize our users with Polar customers without us needing to constantly listen to a webhook. We'll simply be able to query by a customer external ID. In order to properly test this out, I highly, highly recommend deleting all of your existing users. So make sure you are on a clean slate. And now let's go ahead and let's do npm run dev. Let's run the webhook and let's run ingest. And let's go ahead and let's go to the localhost 3000. And also open up Polar Sandbox and go inside of your customers here. Right now you can see I have no customers at all. But now once I create a new account here, I'm going to use Google Login here. something different will happen. So nothing has changed in my app. But if I go inside of customers and refresh here, you will see that I have a new customer with the exact name, exact email, and probably the most important thing here. I have this user's external ID right here. So this external ID matches 100% what I have in my database here. So that is the exact URL. Das ist die exacte ID. So das will allow mich zu einfach Fetch Polar SDK instead of constantly listening and relying to webhooks, which are prone to failing and then retrying and then getting out of sync. It's basically a much, much better experience now. And this will be true for every single new customer that is created. Amazing. This is super, super cool. So what I want to do now is I want to implement the free tier usage into our app, because this user by default is a free user. They haven't upgraded. So let's go ahead and let's create that component here, which will help the user see how many free trials they have, their usage. And in order to do that, we're going to have to implement a router called premium router. So let's go inside of modules here and let's create premium module. So, let's create a server here and inside procedures.es. And inside of here, I'm going to import base procedure, create the RPC router and protected procedure. I'm going to export premium router here. And inside, the first thing I'm going to do is I'm going to create get free usage. So, this will be protected procedure query async. They structure the context here. And then I'm going to attempt to get the current customer by using Polar Client from Lib Polar. And I will simply call GetStateExternal. And I will call ByExternalId, which is basically querying the customer by this, which we already have because it's the ID in our database. So that's the magic of this integration. That's why I told you that inside of my out.ts, this line is probably the most important. And just before I forget, there is one thing we have to do more. Inside of out client, we have to go here and we have to add plugins here, polar client, and import this from polar sh better out. So don't import it from our polar client here. Maybe I shouldn't have named it PolarClient because it's a server thing. I'm using client in a different sense here. Just be careful. Import from the actual npm package and save that file. Now let's go back inside of our premium router where we will now look into free usage here. So once we get our customer, we're going to check if they have any active subscriptions by using customer.activeSubscriptions and simply the first in the array. Why do I know I can look for the first in the array? Because by default, we are not allowing customers to have several active subscriptions. You can enable this and then you would have to obviously change your code to support that. But by default, you can see that is turned off. So each customer can only have one active subscription. And if there is an existing subscription, I'm just not going to show the free usage because there is no free usage. This user is subscribed. Make sure to not accidentally put any exclamation points here. Now what we are going to do is we are going to import the following from our database schema, agents and meetings. And we are also going to import the actual database. And let's also import equals and count from Drizzle ORM. So now what we will be able to do is actually count our usage. So let's start by counting the meetings this user has created. So user meetings and the only thing we are adding in select is count. And we are counting by each meeting ID that we get from this query. Make sure to import count from drizzle ORM. And make sure to add the proper where query so you only count by the currently logged in user ID since this is a protected procedure. Besides the user meetings, we are going to use the exact same logic and query to create user agents. So just make sure to change all instances here to agents, but you are still querying for user ID, like that. And then in here, you will be able to return back to the user the meeting account and the agent account. And let's actually remove the base procedure. We are not going to need it. Great, so now that we have this, let's go inside of the RPC Router's app and let's add this to our router. So premium, premium router, like this. And now that we have that, let's go ahead and let's create a component called dashboard trial. So I'm going to go inside of source, inside of modules, dashboard, components, and I will create dashboard trial.dsx. Inside of here, I'm going to import link from link, rocket icon from Lucid React, use query from 10 stack react query, use trpc from trpc client, button from components UI button, progress from components UI progress. We are using this for the first time. So it's a Shatzian component. We've added it in the first chapter by adding all components. And then I'm also going to do one more thing. I'm going to go inside of premium and I will create a new file called constants.ds and inside of here, I will define the maximum number of free meetings and free agents. Let's reduce this number to three for easier testing. Let's go back inside of the dashboard trial and let's import those two from modules premium constants. Great. Now let's go ahead and let's export const dashboard trial. And what I want to do here is I want to add trpc from use trpc. And then I simply want to load my free usage. So let's use our new premium.getFreeUsage procedure, which we have just created here. If there is no data, I will simply return null here. Otherwise, let's go ahead and let's return a div with a class name border, border, border with 10% opacity. rounded large width full background white with 5% opacity flex flex column and gap y of two then let's add another div with padding of three flex flex column and spacing on the y-axis inside of here open up a div flex item center and gap two and let's go ahead and let's render Let's render the Rocket icon with size 4 and let's render the free trial text. This is enough for us to actually render this component somewhere. So let's go ahead inside of our dashboard sidebar. It's located in the same folder as dashboard trial in the dashboard components module. And then let's go ahead and let's import this down here, the same way we import the dashboard user button. All we have to do is find a place for it. Let's go all the way down to the place where we actually render the user button. How about we go ahead now and we render this, the dashboard trial. Let's see how it looks. Perhaps we will need to modify something. Let's refresh this. There we go. You can see that I have pre-trial text right here. What I want to do is I just want to wrap these two and just give them a class name of p2. Actually, I don't want to do that. So let's just revert to as it was. So let's continue developing the free trial component here. So we are going back instead of dashboard trial. What I'm going to do now is go outside of this div encapsulating the rocket icon and the paragraph. and I will add a new div with flex, flex column and gap y of two. And inside of here, I'm first going to render a paragraph with text extra small, which simply uses data and agent count, which we got from here, right? When we counted our agents. And I am comparing it next to the maximum three agents constant. So it will be zero out of three agents right here. And then what I'm going to do is I'm going to use the progress component just below this paragraph like this. And I'm going to divide the agent count by the maximum number of free agents and multiply it by 100 so it becomes a percentage. Right now this is completely empty. Now copy and paste that entire structure and change this to be meeting count. and this will be max free meetings and change this to meetings and also change the actual function here to work with meetings and data meeting count just make sure you don't use any agent instances here and now you have the meetings down here and the last thing we have to do is we have to add a button outside of this div here so just a button like that and let's go ahead and Let's give it as child prop. And let's give it a class name. So it looks good. Background transparent, border top, border border with 10% opacity. Hover background white with 10% and rounded on top will be none. And inside upgrade button, upgrade text. Let's refresh to see this in action. For some reason, oh, my apologies. Let's add a link here with an href of forward slash upgrade like this. And I think that now, there we go. Now it looks normal. When you click on it, however, it will lead you to a 404 page. That's fine. We will develop that later. But now go inside of agents, for example, and just try creating a new agent. So test, test. And you have to do a hard refresh now. und du wirst jetzt sehen, dass ein aus drei Agents haben sich geöffnet. Und das gleiche passiert für die Meetings. Wenn du eine neue Meeting und ein Refresh ist, die Refresh ist wichtig, denn wir haben nicht das Quere. Es ist ein komplett new Quere, so wir nicht mehr über die Bevorraten. Let's Hard Refresh und dann sehen wir die Meetings-Inkrise auch. Jetzt geht's und wir tatsächlich We can actually throw an error when we hit the limit here, because right now nothing would happen. So we can do that by going inside of source, trpc init. And in here, besides protected procedure, we are now going to create a premium procedure, which will be a function which accepts the entity for which this is a control for, which can either be meetings or agents. And then in here, we are simply going to extend the protected procedure. So protected procedure dot use asynchronous and the structure, the context and the structure next. Like this. Inside of here, let's go ahead and let's grab the customer using the Polar client. Make sure to import one from lib Polar. like this, using our instance of it. It's quite important that you don't accidentally import the other one. And make sure you're using the external user ID here. And now what we can do is we can just do the exact same thing we did in our procedure a moment ago. So we get the user meetings and user agents here. Let's add them here. And we have to import the database. we have to import the meetings schema and we have to import the agents schema and now we have to import count from drizzle orm and we have to import equals from drizzle orm there we go so from drizzle orm count and equals and agents and meetings from database schema as well as the actual database perfect so now we have user meetings and we have UserAgent here. And now let's go ahead and let's check if this user is premium. So is premium if customer active subscriptions is larger than one. And now let's check if we have reached the free agent limit or free meeting limit. And we can do that by comparing the queries above with the actual constants which we have to import. So let's just import them. Make sure you have added these constants. And then we are going to check if we should throw meeting error. So should throw meeting error if entity is equal to meetings and if is free meeting limit reached and if we are not premium. And then the same logic here for agent error. Just make sure you change the entity to agents and is free agent limit reached. And then let's go ahead and check. If should throw meeting error, throw the code forbidden and you have reached the maximum number of free meetings like that and in here after that if we should throw an agent error same thing just the maximum number of free agents and then in here let's just return next and let's add customer in here simply so we have access to it if we need to perfect amazing now we have our new premium Procedure, let's go ahead and use it. So we're going to go inside of our agents procedures, so modules agents procedures here, and let's find the create procedure. And instead of using protected procedure, let's use premium procedure from trpc, and let's select agents here. So make sure you import premium procedure from trpc init for the create method. And everything else stays exactly the same. You don't have to change absolutely anything. And let's make this easier. Let's go inside of our premium constants and I will allow only one free agent for free tier. So you can see that now immediately it shows one out of one agents. So what happens if I try to create a new agent now? I'm getting an error. You have reached the maximum number of free agents. Let's double check that this is working correctly by Let's try deleting an existing agent. So let me just delete this agent. And let's try again. And this time I shouldn't be getting any errors, right? Ignore that this is not updating in real time. We will change that as well. So only after I have actually reached the limit do I start getting these errors right here. And now let's do the exact same thing, but for our meetings procedure. So find the create and instead of protected procedure, we're going to use premium procedure and simply select the meetings. Make sure you have imported the premium procedure. And now the exact same logic is applied to the meetings. Perfect, so what we have to do now is we have to properly resynchronize this every time that we create or delete a meeting or agent. So let's go inside of the agent form. Instead of agent form, I think we already have a to-do here. Invalidate free tier usage. So let's just copy this, let's paste it, and let's go ahead and add trpc premium, get free usage, and remove this to-do here. And no need to pass any objects inside. And copy this from now on. The second place you want to do this is the agent ID view right here. Let's go inside of the agent ID view and find the remove agent use mutation and go ahead and invalidate pre-usage every time that you delete an agent. Then let's go inside of the meeting form and in here in the create meeting we have the same to do. So let's simply invalidate our free usage. And now meeting ID view. Because in here we have the remove meeting mutation. So let's go ahead and do this. Mark this as async and then you can await both of them here. So now if you go ahead and for example delete an existing agent. Let's go ahead and try it out. You will see this immediately clearing up. So let's click delete, confirm. There we go. It's immediately cleared. And if I go ahead and add a new one, it will immediately be refreshed. And now it's one out of one. Amazing. What I want to do now is I want to handle the errors. So once I get this error right here, I want to be redirected. So let's go ahead and do that. I'm going to go ahead and I can actually search for to do probably. And in here, let's go inside of agent form. So go inside of agent form and go inside of create agent here. And we have an on error. So let's check if error.data.code is forbidden. Let's go ahead and redirect the user. We need to add the router, const router, use router from next navigation. Make sure to add that. And then in here simply do router.push forward slash upgrade. And then let's go ahead and do this in one more place, it seems. No, we don't have to do it in the update agent. That's fine. There we go. And now let's go inside of the meeting form. So meeting form, let's find the create meeting and on error, let's do the exact same thing. If the data code is forbidden, let's redirect the user. We need to add the router from use router from next navigation. Just make sure you are doing the proper import here. And you can remove the to do from the update meeting as it's not required here. So now let's go ahead and refresh to see this. If I go ahead and create a new user and click create and I get an error forbidden, I should be redirected. And I am. It just took a while. And it's a 404 page because we haven't developed it yet. So let's go ahead and let's develop it. In order to develop the upgrade page, we have to add a few more procedures to our premium router. So let's go inside of our modules premium server procedures. And above get pre let go ahead and let add get products So it going to be not a base procedure Let change it to be protected procedure like this And we simply going to use polar client SDK to list the products which are not archived and which are recurring. And then let's go ahead and let's return product result items. And then let's go ahead And let's do another procedure here called get current subscription. So in here, we are going to fetch the current user using Polar Client Customers get state external and using the external ID. We are going to check if we have an active subscription. If we don't have an active subscription, we will just return null. Otherwise, if we do have it, we will match the associated product. So we can use the subscription to get an individual product using subscription product ID. And then we will return that product back. Perfect. Now let's go ahead and let's go inside of source app folder, dashboard, and let's create a new upgrade folder inside a page. Let's go ahead and do a default export here. and let's add hydration boundary here. Let's add query client from get query client from 10 stack, my apologies from trpc server, and let's do void query client dot prefetch query. And inside of here, we're going to prefetch our new trpc procedures. So trpc from the server dot premium, and let's prefetch current subscriptions. So get query options for that. And also prefetch get products like that. Make sure you are importing TRPC from its server instance. And let's turn this into an asynchronous method. And let's also add our usual authentication redirect using the out from lib out, using headers from next headers and using redirect if there is no session from next navigation. Perfect. And now let's go ahead and let's add the state to the hydration boundary here. Let's import dehydrate from 10 stack react query. And let's go ahead and let's add suspense from react. Let's add error boundary from react. And let's render the upgrade view, which does not yet exist. Make sure you have added both of these from their correct imports. Double check the error boundary because the IDE often forces you to import the incorrect one. Let's go ahead and let's add fallback here to be to do for both of them. And let's create the upgrade view. So we're going to do that inside of modules premium. And let's create UI. And let's create Views. And inside, let's add upgradeView.tsx. Now, let's go ahead and let's prepare the imports. Use Client is very important. And then use Suspense Query. Then let's add our usual imports. Use the RPC, AuthClient, ErrorState, and LoadingState. Let's go ahead and let's create our upgradeViewLoading and upgradeViewError. So we've already done this a couple of times. We are just reusing our error and loading states with their titles and descriptions. And now we can go ahead and we can import these things from modules premium UI views upgrade view. Let's also quickly add export const upgrade view. And let's just return a div upgrade view just so we have all three. There we go. And let's go ahead now and let's add the suspense to be upgrade view loading. And let's change this to be upgrade view error. There we go. So now when you click on upgrade here, you should finally see the text upgrade view. Perfect. Now let's go ahead inside of here and let's actually load the information that we need. So go ahead and add trpc from use trpc. Go ahead and get the use suspense query for trpc premium get products and map the data as products. And then do the same thing for the current subscription using get current subscription. So these two procedures which we have just created here. So we are finally using them here now. And now when you refresh you should see the loading state or the error state if something goes wrong. Now, let's go ahead and let's give our first div some class names here. flex1, py4, px4. This is unneeded. So just a medium px8, flex, flexcolumn, and gapy of 10. Then inside of that, let's go ahead and add another div like this, which will have margin top 4, flex 1, flex, flex column, gap y 10, and items center. Now let's add a heading 5 element which will render the current user's plan. So heading 5 with a class name of font medium text to excel on medium text 3 excel, you are on d, then we add a space and we add a span with font semi bold and text primary which attempts to read the current subscription name, use the question mark here because this can be undefined, and falls back to free, and then space here, plan. So by default, it will say you are on the free plan. Perfect. Now let's go ahead and let's develop a component called pricing card. Let's go inside of UI and let's create the components folder. Let's create pricing-card.tsx Now in here I'm going to import circle check icon from Lucid React and CVA and variant props type from class variants authority If you're wondering where does this come from we have it in our package JSON because ShadCNUI added it We are going to use it to create different variants of this pricing card because as you can see here We will have a light version and kind of like a exclusive highlighted version. So that's what we are going to use this for. And now let's import all the other imports here. CN from LibUtils, badge from components badge, button from button and separator from separator. We already used all of these in this tutorial. So let's create a constant. Pricing card variance. And let's actually use the CVA which we imported. The first thing we're going to do is give it the default classes. So rounded, large, padding4, py6, and widthfull. And now in here, we open the variants. And we define the prop variant. It can be either default or it can be highlighted. Make sure to properly type it because there is no type safety here. So for the default, it's going to be white background and text black. But for highlighted, it will be background linear to bottom right from this green color to this dark green color and text white. And then we're going to add the default variant down here to be default. und dann wir werden die gleiche gleiche Sache, aber für die Icon Variante. So es wird die gleiche Struktur. Du kannst das und du kannst es. Ich werde die Finish Version zeigen. So, da wir gehen. Pricing Card Icon Variante. Default Class Name Size 5. Variante. Default und Highlighted. In Default Fill Primary und Text White. In Highlighted Fill White und Text Black. with the default variant of default. And then one more variant. Pricing card secondary text variant with the default class name of text neutral 700. And then in the default variant, that very same text. And in the highlighted, a text neutral 300. And no need to add the default variant here. And one more variant, my apologies. This is the badge variant. So this one, this small little badge here. Pricing card badge variants. So by default, text black, text extra small, font normal, and padding of one. And then on default, it will be background primary with 20% opacity. But when highlighted, it will use this kind of beige color. And the default variant is default. Now, let's go ahead and let's create an interface which extends variant props, which we imported from here, and give it a type of pricing card variants. We can use just a single pricing card variants because every single one of these use the exact same variants. So that's why I told you to copy and paste so you don't add any typos. Perfect. And now we're going to add all other props here and the variant props will be automatisch werden wir nicht einfach nur die manuellen. Aber wir haben andere Components, die wir brauchen. Ich meine, die Props sind. Badge, die ist eine Optional String oder Null. Price, die ist ein Nummer. Features, die ist eine Array von Strings. Title, die ist ein String. Description, die ist ein String oder Null. Price, die ist ein String. Classname, die ist ein Optional String. Button Text, die ist ein String. Und OnClick, die ist ein Voi. Jetzt, wir exporten Konz PricingCard. And let's simply destructure all of those elements from the props here. There we go. So all of those elements from above. Now in here, let's go ahead and let's return a div. And we're going to give it class name of CN and simply add pricing card variants and add variant in here. So where do we get the variant prop from? From here. But we didn't explicitly add it here because it was automatically added thanks to variant props from here. Because they read pricing card variants, right? And now let's also add class name here. And let's just add border. Then let's add a div here with a class name of flex items end. And gap x for and justify between. Dann, wir gehen und noch ein ClassName mit Flex, FlexColumn und GapY . Jetzt für die Titel und Badge-Section. Wir haben einen mehr Div mit FlexItemCenter und GapX . So das ist GapY2, das ist GapX2. Der erste ist eine Heading 6-Element mit Font Medium und Text Excel, Rendering die Titel. And then down here, we check if we have the badge passed, because badge is optional. It's not required. So in here, if we have the badge, we render the badge and give it a class name CN pricing card badge variants and pass in the variant and render the badge text inside. You probably don't have this error. This is just the TypeScript server shutting down. Now, outside of this div, which is encapsulating our badge and the title, let's go back inside of this div and let's add an element below it, which will be our description in a paragraph. So this right here. This paragraph will have a default class name of textextrasmall and then pricingcardsecondarytextvariant and passing the variant here and render the description inside. And then we're going to go outside of this div and we're going to get to the body of our pricing card. In here, we will format the price. So let's add a div with flex items end, shrink zero, and gap x 0.5. And then we're going to add an h4 element here, which will render the current price and format it. So an h4 element with class name text3xl font medium. And then we're going to use the native JavaScript intl API and request a number format in NUS style currency with currency US dollar and minimum fraction digits set to zero. And we are simply going to format the price, which is a type of number and a required prop. And then below it, we're going to add a span element, which will render the price suffix, which will be either per month, per year or something like that. And we can style it with class name pricing card secondary text variance. So we already think you use that for the description. So we are using it the same way here. Great. Now outside of these two divs, but still inside of our pricing div, let's add a separator. We are going to encapsulate the separator with a py6 div and a class name opacity 10 and text in this specific color right here. And then let's go ahead and let's add a button, which will be our call to action. So, render the button text inside, which is a prop. And now let's go ahead and give it some attributes, class name, full, size, large. And let's go ahead and let's give it a variant, which will be, if variant is equal to highlighted, use default, otherwise use outline. And onClick will simply be the onClick prop. There we go. And now, outside of this button, we're going to render a div, which will hold our last elements, which are the features. So flex column, gap y2 and margin top of 6. So inside of here, let's add a paragraph, which will simply say features in medium font and uppercase. And then let's create an unordered list. So this unordered list will have a class name of flex, flex column and gap y2.5 and then pricing card secondary text variants, which we've already used two times here. And we're using it in the exact same way here as well. And in here, we are very simply going to iterate over our features. Let me just properly add this, like this. So features are optional, right? Actually, they're not optional, so you can do this. My apologies. Go ahead and put this. Features.map, feature index, list item with an index as the key element, class name with flex item center and gap x 2.5, Circle check icon with pricing card icon variants and rendering of that feature. That's it. Let's see what we haven't used yet. So let me just see in here. I think I'm doing something incorrectly. Let me just check. So let me just check this. Let's try reloading the window. Is that the issue? Am I not seeing here? I think I'm doing some incorrect parsing here. So let's see, this, am I closing this tag anywhere? I am. Am I closing the return tag? I am closing that too. So I think I have something opened here, but I'm not exactly sure what. Oh, it's the question mark. My apologies. So this will be not feature. This is features, I think. Is it? Let me go inside of the props here. Feature, I think this should be called features. So let's call this features and let's remove the question mark here. Let's call this features here. And then down here, when I iterate over the features, I think I can just use it like this without the question mark. Great, that is our pricing card. So now let's go back to the upgrade view. And what we can do now is go below the heading 5. And we can open up a div here with grid, grid columns 1, on medium grid columns 3, and gap 4. And we can go over our products. So products.map, get the individual product here. And then what we're going to do is the following. Wir werden erst einmal checken, if isCurrentProduct, using CurrentSubscription?ID mit dem ProductID. So wir werden jetzt schon mal die Produkte verabschiedet. Dann, wir werden jetzt schon mal die Produkte verabschiedet. Dann, wir werden jetzt noch eine Art und die Produkte verabschiedet. Und dann, wir werden die ButtonText, die, bei default, wird Upgrade. Und die OnClick-Method wird, bei default, be AuthClient, which we'll call checkout and simply add products, product ID here. So it's that easy to open a checkout. We can use it through auth client here because in here we added the polar client plugin. So that easy for us to open a checkout page. But now we're going to add some if clauses. So if we are looking at the current product, the button text here will be manage And onClick will instead open Customer Portal. ElseIf is Premium. So if we are not looking at the current product, but we are looking at a Premium product, we will simply add Change Plan text, which will also open the portal because that's where you can actually change the plans. And in here, let's go ahead and let's return the Pricing Card component. You can import the Pricing Card component from here. and then let's go ahead and let's just give it all the props. So let's give it key of product ID. Let's go ahead and let's give it button text of button text. On click to be on click. Now the variant will be controlled by product metadata variant. And if we explicitly say in the Polar dashboard that this should be highlighted, we will use the highlighted variant for this card. Let's go ahead and give it a title to be product name. And let's go ahead and give it a price. So we go instead of this individual product, first price in the prices array, and check if the amount type is fixed. If it is, we go into that price amount, and we divide it by 100. Because if you tried just doing this, price amount by default is a type of any. So you have to check that the price that you're trying to get is actually fixed because Polar has options for pay as you want and free. So that's why price amount is only a number if you first check that the amount type is fixed. And you have to divide it by 100 because they store the price in cents. Then let's go ahead and let's add product description. and let's add product price suffix here by opening backticks, add a forward slash, and then access product prices first in the array and then just recurring interval. And then let's go ahead and let's load the features which we're going to use benefits for. So product.benefits.map and simply load the description of each benefit. And then let's go ahead and let's add a badge here as product.metadata.batch. There we go. And that is actually it for our pricing card component. What we have to do now is we have to actually create some products. So let's go back inside of our sandbox and let's go inside of products here. So the first product I'm going to add is going to be a monthly product. And the description will be for teams getting started. My pricing will be monthly and it's going to be a fixed price. And then just put whatever you want, for example, $29. And then I'm going to go ahead and I will add, let's go inside of the metadata here. And I have to add some automated benefits here. So we're going to click custom and click create new. And then in here, go ahead and create a few benefits. So actually, let's not do it like this. Let's just create this very basic product now. So click Create Product. And now you have the monthly product. So right now, if you try, I'm not sure if it can load or not. There we go. So it loads, but not as perfectly. And it's missing something here. So let me go inside of my pricing card. Okay, this should be Features. Let's refresh. So we are going to showcase this with features in a second. Now what we have to do is we have to go inside of here and click on benefits here. It's easier to create them from here. And now the first one will be unlimited agents like that and give it a type of custom here and click create. Then let's go ahead and let's create another benefit with unlimited meetings and create. And now let's go inside of our products here, inside of monthly, and find the automated benefits section, open up custom and simply enable unlimited meetings and unlimited agents and click save product. und all existing ones will now see this too and there we go, unlimited meetings and unlimited agents right here and you can of course add as many of these benefits as you want you can see that in here I added unlimited transcripts, unlimited recording storage right and now let's go ahead and let's add the yearly product so I will add a new product here and I will add yearly here and this one will be a yearly subscription fixed price and i will put 259 dollars a year here and what i'm going to do now is i'm going to go inside of metadata and i will give it a key of variant and i will give it a highlighted value so if you take a look at our code in the upgrade view If product metadata variant is highlighted, we highlight that product. So make sure to not misspell this. And I will also add a badge here to be best value like this. So now you can see that when I go into my badge, I'm reading metadata badge. So make sure to add those. And let's enable unlimited meetings, unlimited agents. And let's go ahead and let's also add a new benefit to months free. Because we are technically giving them two months free if you do the math of monthly subscription and yearly subscription, right? So now if you refresh this. Oh, did I save it? I did not create the product. So let's create the product here. There we go. Let's refresh. And you should now see, there we go, the highlighted product with best value text here. Perfect. Let's go ahead and add one more simply so you can see what else you can do. So this one, oh yeah, and one thing I forgot to do for yearly was to add a description. So let me just add for teams that need to scale and let me just go ahead and save the product. And now you should see the description here as well. There we go. And now let's go ahead and add one more, which will be called Enterprise. So this I'm doing just for fun, simply so you can see one cool feature that they have. So let's add for teams with special requests. It's going to be yearly only fixed price and let's put $1,000 a year. And what we can now do here is we can enable two months free unlimited meeting, unlimited agents. And you can also add dedicated Discord support. Und du kannst das eigentlich ein Discord-Invite und dann eigentlich connect dein Discord-Server und dann addet die Rolle. So, wenn sie sind, sie haben die Access zu dein Discord. So, für jetzt, ich werde Custom-Custom sein, aber die Punkt ist, dass ich die Discord-Invite hier und connecte es. Und dann, wer subscribes zu $1,000 a Jahr hat, wird die Access von deinem Group. So I will now save this as is. I will refresh. And there we go. Perfect. So now let's try and upgrade. So right now I'm on the free plan and you can see that in here, when I go into customers John Doe currently zero revenue here I don have any active subscriptions But if I go ahead and upgrade for example for yearly here there we go I'm just going to go ahead and add, you can add the same dummy information like you do in Stripe. So this is a test card, which always succeeds. Let's go ahead and let's select a country. And once you select a country, additional tax will be calculated here. And let's click subscribe. And once you do that, you don't have to wait for any webhooks. You don't have to do absolutely anything. The reason you don't have to is because we added the external customer ID, which basically tells us now that we are on the yearly plan. And did you notice we no longer have the free usage? We can now create as many agents as we want. Let's go ahead and let's try and create another agent here. No error. We are able to create that agent. And from here we can see the text change plan, which takes us where to the customer portal. And from here we can actually change plan to anything else. And you can see all the grants that we have. Perfect. So it's that easy to do all of this with Polar and BetterOut. And let's also enable the billing button here as well. So all we have to do is go inside of the dashboard user button. we already have the alt client import so we can go down here to the billing and simply do alt client customer portal like that and that's it click on the billing and that will redirect you to the portal at least it should did i do it correctly so let's try it again let's wait a few seconds oh the reason i think it's not working because i'm i'm doing this in the drawer i forgot that we do if is mobile so yes you have to do it both for mobile on click and in the drop down here whoops so let's just add it here drop down menu item and now we can confidently try it let's Click on Billing and this should now open the Billing. Let's refresh first. Oh, okay. It did it. It just took a while. Perfect. Amazing. That's it. Our app now has Billing. Our app now has absolutely everything. Amazing, amazing job. So let's go ahead now and let's mark all of these as completed. We added Polar SDK. This, this, this, this, and this. Now let's go ahead and merge this. So I'm going to go ahead and create a new branch called 27 payments. I'm going to stage all of these changes. 27 payments. Let's commit. Let's publish the branch. Now let's go ahead and open our GitHub. Let's create a pull request. And let's review it. Und hier haben wir die Summary. So, wir haben eine Subscription-Upgrade-Page mit Detail-Pricing-Options- und Upgrade-Workflow. Wir haben eine Sidebar-Komponenten, die Free-Trial-Usaage und eine Prompt-Urgabe. Wir haben eine Premium-Feature-Gating-For-Creating-Agent- und-Meetings- mit Clear-Upgrade-Prompts, wenn die Limit-Urgabe-Recht sind. Wir haben eine Billing-Portal-Axis von der Dashboard. and we displayed current subscription status and available plans in the upgrade screen. The free usage limits for agents and meetings are now enforced and visually tracked by the sidebar component. Usage data and trial status refresh automatically after relevant actions such as creating and deleting meetings and agents. And that is actually the cache invalidation here, which updates the UI. So exactly what I just mentioned. Perfect. So in here we have amazing diagram if you need visual explanation of what we just implemented here. So it's so amazing how Polar, how CodeRabbit constantly has context of everything we do and just builds on top of that, right? So this is a very, very nice way to summarize what we just did using these visual graphs right here. And I reviewed the comments here. Most of these are just refactoring issues. And some of it is just older documentation such as this. We do have one potential issue, but I use this completely normally inside of my app. So I don't think any problems will arise from this. Same thing with this. It's the same issue. In here, yeah, this is a little messed up. We could probably improve this somehow. But as long as you created your products exactly as I did, you won't have any issues here. So let's go ahead and let's merge this pull request. And let's go ahead and go back to our main branch. And let's go ahead and synchronize the changes. After that, we are going to see 27 payments merged into our app. Perfect. Amazing job. And see you in the next chapter. In this chapter, we're going to prepare our app for deployment by fixing the last of the bugs we have left. This will include implementing the proper dashboard command, adding proper meeting count to agents, and testing the build. Ensure that you're on your main branch here, and let's go inside of dashboard command. In here, let's go ahead and let's add the following imports. UseRouter, UseQuery, and UseState from React. And then let's also add UseTRPC and GeneratedAvatar. Let's go ahead inside of the Dashboard command and let's prepare UseRouter and SearchState. In here, let's go ahead and create TRPC instance and also immediately query meetings and agents using meetings get many and agents get many. Make sure to pass the search prop and increase the page size to 100 for both of them. Now in here, add should filter to be false, like that. In here, you can add three little dots here and go ahead and give it value search and on the value change to modify set search. And then let's go ahead and let's add a command group item. So you can import this from the actual components UI command and give it a heading of meetings. And inside, add command empty, no meetings found. And you can import this from the same folder. Now, let's go ahead below the command empty and let's go over meetings data?items.map and then simply add on select to be router pushed to the individual meeting and set open to false and render the meeting name like that. Let's go ahead and let's remove this test items here. And now, let's go ahead and do the exact same thing. So I'm going to copy this command group here, Paste it below. And in here, the heading will be Agents. This will be no Agents found. And this will use Agents data. And this will redirect to Agents, like that. What we have to do now is besides the name, also render the Agent. Whoops, let's change this to Agent. So this is then Agent ID, Agent ID, and Agent Name. Like that. And I'm pretty sure that is it. We can now query for both of them. So let's quickly check inside of our app. Make sure you have your app running here. And make sure to have some data. So let's see if I have the agents here. I do. So if I go for a specific agent, I can have found it. And when I click it, it should redirect me to it. There we go. And the same is true for meetings. I just happen to not have any meetings here. Let's go ahead and create one. And once you do so, you will be able to find the meeting here. There we go. Amazing. So we completed that. Now, if I go inside of my to-do, I have added this inside of dashboard meetings meeting ID, prefetch meetings get transcript. We don't need to do that because in the actual transcript, we decided to use useQuery instead. Now let's go ahead and handle the actual count of the meetings that the agents are used in because this currently always displays the number six. So what we are going to do is implement the proper meeting count using database $count and simply import the meeting schema and look for meetings agent ID, which matches the current agent ID. So make sure you imported the meetings schema here. And you will have to do the exact same thing in the get many procedure. So let me scroll down here, find get many, and I have the same thing here. So let me just add it here and I will move it after get table columns. And in get one, I'm pretty sure I also need gettable columns here to give me the entire agent. There we go. So now, as you can see, this says one meeting, simply because in here I have set it to this meeting right here. This one says zero meetings, and it's the same information if I go inside. You can see it just refreshed to zero meetings. Great. So I think that's it. Das ist alles wir haben hier zu fix. Und du kannst den SQL aus hier rausnehmen. Und jetzt, wir gehen jetzt und wir machen. Actually, es ist eine weitere Sache, wir müssen uns machen. Und das ist die Möglichkeit, die zu cancelen ein Meeting. Aber, truth be told, ich habe keine Ahnung, was die Worte der Worte des Cancelen. Initially, ich dachte, das wäre ein tolles Idee, zu einfachen, aber du kannst wirklich viel mit dem. You will eventually just delete the meeting, right? So that's why I don't see much point in giving the user an option to cancel the meeting, right? Now in hindsight, it just doesn't make sense. Like what does the user do with it then except delete it, right? So let's go ahead inside of upcoming state here. And let's just remove this button. And let's remove on cancel meeting. So you can just start it. So then find where you render upcoming state, which is in the meeting ID view, and remove on cancel meeting and remove is canceling. We don't need is canceling either. So remove this too. And remove the ban icon. I think this makes more sense because what's the point of canceling a meeting, right? You can just delete it if you don't want it anymore. And I'm pretty sure that is it. Ich habe vielleicht etwas, aber ich glaube, dass ich seit 13 Stunden, ein Tutorial habe, und da gibt es einen Error, aber ich glaube, das ist ein Hot Reload passiert. Jetzt, nach 13 Stunden, ich bin sicher, dass du eine Bugge findest, du kannst es selbst. Wenn du findestes etwas sehr, fühle mich, oder send mir ein E-mail, so ich kann mir ein sehen. Amazing, amazing job. Und jetzt, wir gehen und wir machen das folgende. You can shut everything down here, including this. And let's do npm run build. So let's just check if this throws any errors. Now, I don't seem to be having any errors, so this completely worked. If you do have any errors, you will have to resolve them before you can deploy the app. Perfect. So now let's go ahead and let's merge this. So I'm opening a new branch here. 28 bugs I'm going to go ahead and stage all changes I will commit 28 bugs, commit, publish the branch and since this was super simple, there's really no need for any review here we're just going to go ahead and create a pull request just to be consistent with the rest of our pull requests and let's immediately merge this pull request and then you can go back inside of your main and you can click synchronize changes right here. And in a second in your graph, you should see 28 bucks right here. Perfect. So in the next chapter, we're going to go ahead and deploy. So last chapter, deployment. Our goal is to add the project to Vercel, add the environment variables, change the webhooks to new Vercel URL and connect ingest. Let's start with the first one, which is add the project to Vercel. So, if you followed along with my Git workflow, you will most certainly have your GitHub repository here. If you don't, you need to add your project to GitHub. You can see how I do that in the first few chapters, right? So, you need to have your project here. After that, go ahead and create an account with Vercel. Go inside of your project and click Add New Project. Connect it with your GitHub and you will have access to your project here. Let's go ahead and hit Import. Open the environment variables, open your environment variables here, copy all of them, and simply paste them in the first field, and that will populate all of them. Let's go ahead and let's click deploy. We will not be done yet because, for example, we have to fix this. This is incorrect. We also have to fix this. That's also incorrect. So let's go ahead and click deploy here. After a few seconds, we're going to see the result. Maybe we're going to see some errors in the logs or it will succeed. Regardless, we're going to proceed forward after we see the results here. For example, I immediately found a bug here and I think I know why. So I think I know exactly what I have to do. Let me go back to my project here. Let me go inside of settings and let's go inside of build and deployment. And install command, let's override it. And let's do npm install dash dash legacy peer depths. And let's click save. Now let's go inside of deployments here and let's redeploy. And click redeploy here. I'm going to follow closely along and see if we have any more errors. And there we go. Seems like adding that override npm install legacy peer depths resolved all issues. And now I have the new domain here. So this is my main domain, right? When you deploy, you will see these other domains. Make sure to go inside of your project here, overview and find your domain here. This is your main domain. So what you have to do now is go inside of stream, go inside of your app. And in here where it says updating these settings will affect both chat, video and audio. You have to modify this to no longer be your webhook. But instead, be your actual app. Let's just remove the sign-in part. There we go. Your actual app. Now keep in mind that this will then break development. So the good thing to do would be to create two projects and then use different keys for both. But I'm just trying to simplify it for the tutorial. So now change this, but be mindful. This will break development. So later you're going to have to revert this back to your, whatever it is in the package.json, right? Back to this. So please remember that. Write it down if you need to. And let's click Submit here. There we go. That changed. And now we have to do one more thing. And that is go inside of our project here, inside of Settings, inside of Environment Variables. And let's go ahead and let's find the BetterAlf URL. Let's edit it and replace it with our deployment now. Let's click Save here. Then we have to do the same thing for our next public app URL. Edit it and replace it and click Save. Looking at my environment, I think those are the only instances where we used our localhost, so we have now changed it to this. Great. Now what I want you to do is go inside of deployments and click Redeploy one more time. So what this will do is restart the project, but with new environment variables set. So now the authentication will work and the TRPC will work as well. After you have redeployed, here are the next steps. So right now, if you try Google or GitHub login, it will not work. That's because we have to go ahead and actually fix something here as well. So let's go inside of our GitHub developer settings here. And in here, find the homepage URL and just set it to this. And in here, change the localhost part to be HTTPS API out callback GitHub and update application. Keep in mind that this will break development. So it would be a good idea to create a different app to handle that. And you have to do the same thing in the Google Cloud out platform. So go inside of your clients and in here change this to your new URL and click save. And let's also go ahead inside of branding here and find authorized domains and add the domain here. And let's just do this and click save. And besides this, let me just double check in the clients. I think we also might need to add authorized JavaScript origins and just add this here and click save. You can never be too sure with Google out. Okay. Then one more thing that we have to do is we have to properly connect ingest. So go ahead and create an account, create an organization, and then go inside of your apps and click take me to onboarding and select sync with Vercel. In here I'm going to click Manage Vercel Integration and I will click Connect Vercel to Ingest. I will select my free account here and I will click Install. Let's go ahead and select our project and click Save Configuration right here. That will add Ingest Signing Key and Ingest Event Key. Let's go ahead and click Continue here. Und du kannst du sehen, dass alle diese Projekte nicht haben, also es nicht werden, aber es ist für Meet AI. Hier steht, dass es Ingest nicht möglich zu kommunizieren mit dem App. Let's click Learn More hier. So Bypassing Deployment Protection. Wenn du Vercelles Deployment Protection hast, by default Ingest nicht möglich zu kommunizieren mit dem App. Okay, so let's go ahead and see how we can work this. So we can either disable deployment protection or we configure protection bypass, but this is only for premium projects. So it's better to learn how to do this because you can do this for free once. So let's go ahead and learn how to do this. So basically, here's what you can do. You go inside of your Vercel dashboard in your organization. You go inside of settings here, and in here you find deployment protection. And you can see that I have deployment protection for Meet AI. So when I click here, it redirects me to the project settings here, deployment protection. And in here, I can either turn it off completely, or I can create a protection bypass for automation. So this is exactly what the ingest would need. So I'm on my free account here. So I'm going to try to do that, but I'm not sure if I can do it. So I'm not going to select any value here. Let's just click Save and see what happens. That creates a value for us here. So let's copy this. If you fail at doing this, you can just disable it altogether. So now it says to do the following. We copy the secret and we go to Vercel Integration Settings page in the Ingest dashboard. So let me see how I got here. Let's just see. I'm trying to find this. So inside of here, we have integrations, Vercel, Manage. And now in here, let's find Meet AI and let's click Configure. And in here, let's add our deployment protection key like this. And now let's go ahead and let's redeploy our app because I think that's the next step. So I'm going to redeploy our app and then I will simply try and use our app. So let's go ahead and redeploy one more time. And after I redeployed, if I look here, it says it has successfully connected here, right? And it has the function meetings processing, which is basically our function to process the meeting. So the only thing left to do really is to try our app. So I'm going to go ahead and check, do I have any agents? I do and I'm just gonna go ahead and actually I don't know start this meeting let's go here I will allow the camera and everything and now I'm going to allow my microphone and I'm going to try and do a conversation here and there we go so I just ended the call let me just show you it's in processing status here And you can see I have ingest running here. And it's gonna go ahead and do the steps. So let me go ahead and check this out. So you've probably noticed that my ingest steps are the color red, indicating that they have failed. So what's happening here is my initial step, step.fetch, is actually failing. So what you can do to fix it, if you have the exact same error, is simply turn this into a normal fetch. So go ahead and add step.run fetch transcript and simply call your normal fetch method and then return it into text format. And then in your next step, you can go ahead and remove what was the response.text transformation here. Because now we get it directly from this fetch return. So you can just pass the response inside of jsonl.parse and then continue with all other steps as normal. You can then go ahead and commit these changes, push them to your main branch, either via a pull request or directly, and then redeployment will start automatically and go ahead and try again. One last thing we can do before we end this tutorial ist, dass die Home-Viefe von der Home-Page ist. Weil jetzt, wenn wir uns jetzt auf das Dashboard-Page.tsx ist, dass es eine Home-Viefe, die gleiche Text im Hintergrund ist. So es ist nicht wirklich gut für uns. So was du kannst, ist, dass du in der Next.Config.ts und hier kannst du hier eine Asynchronous Redirects Funktion. which targets the root page and redirects everyone to forward slash meetings. And you can set permanent true or false. Both will work. But be careful with permanent true, simply because that is a specific instruction on the internet, right? That's how your website will be indexed. So you can research that a little bit. Also, once you do this, if you test it out locally, it can happen that when you open your new project with Next.js, your localhost immediately redirects to forward slash meetings. So if that keeps happening in your new projects, which don't have this set up, just clear your cache, open incognito mode, clear cookies, just do something like that and it will stop doing that. Or I think that you can just do permanent false and then it will not be as strict. And then after you actually do this, you can go ahead and you can completely delete the page inside of dashboard. Your app will function just normally without it, right? And this will simply always redirect your app to forward slash meetings. so it won't even look for that initial page. So go ahead and commit this and push to your main branch and redeployment will start automatically and go ahead and test your app. And that marks the end of this tutorial. Amazing, amazing job. And thank you so much for watching and see you in the next video.",
  "transcript_chars": 83205,
  "transcript_filled_at": "2026-05-25T02:35:28.499113+00:00",
  "transcript_filled_by": "groq"
}