In this chapter, we're going to add some API improvements. This will include finally creating the refresh method for our contact session. We are then going to actually use that new refresh function to refresh the contact session when needed, which is whenever user is active. You will be able to decide for yourself what you think should count as a session refresh. Basically it's a way so we don't delete the user's session because remember user sessions have a expired time, right?
So if you want to prolong that, if the user is active, if the user is sending messages or opening new conversations, it would make sense to prolong that expiration so after 24 hours passes they don't come back and all of their conversations are gone. And then we are also going to use our previously created subscription stable to protect some API functions for pro only features. Let's start by creating the refresh contact session method and let's kind of refresh what and how this even works. So if you go inside of packages backend convex public contact sessions In here we have the create method. So this is how we create contact sessions.
And in here you can see that this lasts for 24 hours in milliseconds. So let's go ahead and see what happens. So we set it to last 24 hours, right? And after a certain time, it expires. What happens then?
Well, exactly what's happening to me right now, even though I recorded some chapters yesterday, you can see that now my session is completely gone. Well, that's not really the greatest experience because I had some chats open, I sent some messages, it would make sense that my session was prolonged for at least another 24 hours because of that. So every time I send a new message, if I am within the expired threshold, it should prolong. So it shouldn't just add 24, 48, up to infinity, of course, but it should have some kind of threshold. If I'm very close to expiring and if I'm still active, I should just add another 24 hours to this account just in case, right?
So let's go ahead and just remind ourselves how that works. So go inside of this create method and just briefly modify this. Instead of this, let's go ahead and just use, I don't know, 10 seconds like this. So 10 times a thousand. So now if I go ahead and create 10, 10 at mail.com, you will see that this works just fine.
I can refresh. I'm still here. I can still join here. Everything works. But after 10 seconds, if I wait, if I wait some more, some more, some more, I will eventually have to log in again.
My session just expired after 10 seconds. So for now, I want you to keep this at 10 seconds or maybe 30 seconds if it's easier for you to work with so you have a visualization of how our session expires. And now let's go ahead, let's go inside of system and let's go inside of contact sessions. In here we should have get one. And now we're going to add a new one which we're going to call refresh.
So export const refresh is going to be internal mutation like so. It will have some arguments. Let me just close this. There we go. It will have arguments, contact session ID, which is a type of V.ID contact sessions.
We will have a handler, asynchronous context and arguments. Whoops, okay. And inside of here what will happen is, first things first, let's attempt to get the contact session by doing await context database get arguments contact session id. If there is no contact session, let's go ahead and throw new convex error from values with a code not found and a message contact session not found. Then let's check if it's already expired.
So if contactSession.expiresAt is less than date.now, There's no point in refreshing this. It already expired, right? So we'll just throw back an error here and let's give this a code of expired, contact session expired. Or maybe bad request would be the better type of code to throw here. And now if it is not expired, let's see how much time we have remaining.
So time remaining here is contactSession.expiresAt minus date.now. And now in order for us to decide do we refresh this or not we have to check if we are within some kind of threshold to refresh. So how about we make it so that the auto refresh threshold is four hours. So if we have four hours left and the user is still active, we are going to refresh this and give them another 24 hours. Why four hours?
I don't know. I just feel like that's a logical number. You can put 10 hours. You can put if they have 12 hours left. You can put if they have 23 hours left if you want them to have the best experience.
But do keep in mind, the longer you keep the session open the more it is susceptible to some kind of session attacks right. So just for now it's also copy the session duration here exactly this one which we just modified So make sure the session duration is exactly the same as in your public file here. And now let's finish this method here. So once we have the time remaining, let's check if time remaining is within the auto refresh threshold. Let's create the new expires at which will be date.now plus the session duration in milliseconds which I just copied here from here that's why it's important that it's the same.
So we renew the entire expiresAt property and then we can do await context.database.patch arguments.contactSessionId expiresAt new expiresAt. And let's return expires at and let's return contact session expires at new expires at and let's return contact session otherwise. Great. So now we have a method that we can use to refresh the session. So let's try it out.
I think the most logical way to use this refresh is whenever a user sends a message, right? So in the public messages here, Let's find the create. So if the user sends a new message after we confirm that this conversation exists after we confirm everything here is actually fine. Let's go ahead and do a weight context and I think that we have to do let me just see is this a mutation I already forgot it's a mutation so I think we can just do a weight context dot run mutation internal dot system dot contact sessions dot refresh and passing the contact session id to be let's see what do I actually have I have arguments dot contact session id perfect arguments dot contact session id So I will add a little comment here. This refreshes the user's session if they are within the threshold.
So the user at this point, since they're sending a message, is obviously active, right? But their session could expire soon. So if we detect that the user is active and their session is about to expire if they have less than four hours left we are going to refresh this for them. So let's try this out now. So I keep talking about this four hours but this actually makes no sense because I just returned this to be 10 seconds.
So let me just modify this too. Instead of four hours, I'm going to set this to be, I don't know, if we have five seconds left. So if the user is inactive for five seconds and then sends a message after 5 seconds, I'm going to grant them a new expiry date. So let's check that out. So first things first, I'm going to go ahead and try this again.
So test1, test1, and mail.com. Test1, test1 and mail.com. And let's try 1, 2, 3, 4, 5, 6, 7, 8, 9, 10. I believe that now after I refresh I have to create a new account. But this time I'm going to try something else.
I'm going to attempt to send a message. So 1, 2, 3, 4, 5 and let's send a message. Test 6, 7, 8, 9, 10. At this point, if I refresh, I should be logged out, right? Except I am not.
I can still type here because I actively refresh my session every time that I send a message. But if I don't do absolutely anything again for another 10 seconds, I will have to enter my credentials again and start from scratch. This obviously feels very short in this example, but that's because we made this to be 10 seconds and this to be five seconds. In a real world example, we set this to 24 hours and we set the threshold to four hours. So if someone has four hours until we expire their session and we notice, hey, they are still sending messages, they are opening new conversations and they only have four hours left, let's give them another 24 hours so they can relax.
You can see that now when I refresh I'm fairly certain again I have to create a new account. So we just proved that this entire thing works. Amazing. Now let's go inside of the contact sessions. Let's bring this back to 24 hours here.
Let's copy it. Let's paste it here in the system. So it uses 24 hours here as well. And yes, let's also fix the alpha threshold MS to be 4 hours. So this is 4 hours in milliseconds and this is 24 hours in milliseconds.
Let me find, is there a way I can do, instead of convex here, Let me go ahead and create constants. Constants dot ds. And I will attempt to just once write this instead of that new constants and export it. So export session duration milliseconds. And then let me try and maybe import it.
Can I do that? I think that I can. And I will also do it here. So now I don't have to type it into places. I can just import it from constants.
Right. Let me check. Everything seems to be working just fine here. Perfect. So now if I am active within 24 hours of my expiry, I will get another 24 hours.
And now it's up to you to decide what should be considered a refresh. So when the user sends a new message, it makes perfect sense. Let's refresh the user session, right? Let's run this internal mutation. So you can do this for anything you want really.
I wouldn't really do it for get many, maybe that's a bit too much, but you could if you want to. This way every time this contact session ID fetches their messages, it will be considered as hey, they're active. No reason to remove their session, right? Another place you can put this, for example, would be conversations in the public folder. Let's try and find this.
So not to get one, let's try and find create mutation here. There we go. So after you confirm the session, yeah, already here, I think this is good enough. Let's go ahead and do this, right? So if the user creates a new conversation, why wouldn't we refresh their session, right?
That makes sense. Just make sure you have imported the internal. And just like that you have improved users experience on the widget side because now they have longer lived sessions. Perfect. So we just completed this and we completed this.
And now let's learn how to protect our functions for pro only features. So the most logical place to check this for is in when we send messages. That's right in the public folder right here. Public messages. And in here we actually have a to do, implement subscription check.
So when should we actually trigger support agent response? Because this is how we decide whether we should trigger the response or not. Well, right now it's only if the status of the conversation isn't unresolved. But now let's enhance this by adding a new thing const subscription await context run query internal dot system dot subscriptions get by organization ID and passing organization ID to be conversation dot organization ID. So we have all of those things.
Why are we doing run query and then calling internal here? Just a reminder create is an action. It doesn't have access to the database. So now that we get the conversation from here, right, using another internal query and we confirm we have it, it means we have an organization ID and then we can use our previously created internal system subscriptions getByOrganizationId, which very simply attempts to find the organization subscription using the index and returns a unique subscription. And now what we are going to very simply do is improve this should trigger agent by also checking if subscription?
Question mark dot status is equal to active and this is just single subscription like this. And we can remove this to do now. So we are only going to call support agent.generateText as in respond to this user's query if the conversation status is unresolved and if this organization actually has a subscription because our AI responses are a premium feature for this SaaS. Of course, you can decide this logic for yourself but I'm just using the most logical one. So let's try it out now, shouldn't we?
I'm going to go ahead and create a completely new organization and I'm going to call it free as in this is my free organization, right? And then I'm just quickly going to go inside of the conversations here and I'm going to go inside of the clerk dashboard and I will copy the organization ID for that new free organization because it's important because once I have the organization ID I can go to my localhost three thousand and one and in the URL I can change my organization ID param to use that new organization ID. So in here I obviously have to create a new account because this would be a brand new website that you would encounter this widget on. So I'm going to be Antonio Antonio at example dot com and I'm going to click continue. As you can see there are no options here because this is a brand new organization so no voice features nothing And I'm going to click start chat.
And sure, this is the start message, but this isn't AI, right? This is us hard coding a hello message. I think this is fine to have all the time, right? This is the bare minimum. The chat box should greet you in some way.
Now let's go ahead and do checking if AI will respond. And as you can see, no response. That is because this organization is free, right? So No AI can respond, but luckily the actual operator can respond. So automatically this turns into escalated because that's how we decide the flow will go.
The flow is definitely more tailored towards pro users but you can still, it works pretty fine with free users as well. So yes, AI does not respond now because subscription is not active. But Let's see what happens if they decide to upgrade their free organization. Let's subscribe to $29 a month. Looks like we have some bug here but let's go ahead and just continue with the payment for now.
There we go, payment was successful. And let's see what happens now. So the first thing that should happen is our convex dev should now have a new subscription table for our free organization. So instead of echo tutorial, If I go inside of my data here, inside of my subscriptions, I have two of them. And both of them are active.
The previous one was active, it's kind of a bug actually, but the new one is active for real. And if I try this now, start chat and say, hey, can I talk to AI? Let's see. There we go. You are already talking to an AI.
So it works because we just upgraded. So the internal subscription that was found was checked to have status active and the conversation status was unresolved which is by default which means should trigger agent turns the true and now the AI responds. Amazing! And now you can use this exact logic here to protect whatever you want internally. For example, we do have some things we can protect.
Inside of the conversations here, we have this feature. What do you mean, For example, we have this enhanced feature, right? And when we click on it, it spends our API tokens. I think those are the most important things to protect with an organization, with a subscription check. So let's see, where do we do that?
It's in private. Is it in messages here? It is, enhance response here. So after we get the organization ID, let's do the following. Let's attempt to get the subscription using our internal system here.
And let's simply use the org ID. And then if there is no subscription question mark status, my apologies, if subscription question mark status is not equal to active, let's throw new convex error here. Code, bad request, message, missing or maybe I don't know missing subscription. Right? Whatever.
And let's try this again now. So since in my ironically free organization, I still have a premium. Let me refresh now and let me try this. Hiya, enhance. Does it work?
It works. Hello, how can I assist you today? Perfect. But if I go ahead and start another organization test free again this one is now completely empty. Let's try this again.
So I just have to go to the organizations. I have to go ahead and copy the organization ID. I have to go to my widget, I have to change the URL ID here. There we go. So new new mail.com, I'm a completely new user on a completely new website.
Hi there. First of all, AI not responding, great. Second of all, if the operator who is on their free tier attempts to use the enhance, let's see what happens. So again, the operator wants to click enhance and we get an error, bad request, missing subscription. And we can enhance this even further by showing a toast message but I think you get the gist right you can now reuse this internal query and simply call this exact thing anywhere you need right So for example, in the files in here, when we do add file action, you can do the same thing.
After you confirm the organization ID, same thing. Check if they have a subscription because this is a premium query. So import internal from generated API. And if the subscription isn't active or it doesn't exist, throw an error, missing subscription. You can't upload a file, right?
Or if they want to, well, you should always allow them to delete things. That's just, you know, being nice because otherwise they are kind of locked in and they have to pay. So don't, don't protect the delete file. That's always a helpful thing to have. But if they want to add new files, yeah, you should throw an error because that's a premium feature.
Like you can only upload if you are on premium. And you can of course go through whatever you want here. Secrets. Should you allow them to upsearch secrets? Should you allow them to add new plugins right you can protect all of those things you will decide it for yourself but you'd use the exact same method like this of course I'm fairly satisfied with how it is right now the only thing I want to do is just fix this So I'm going to go inside of my conversation ID view and somewhere in here I should have my enhance right here.
So on click handle enhance response And in here let's do toast from Sonar dot error. Something went wrong. There we go. So just make sure you import toast from Sonar. So if you try again.
OK let's refresh maybe and try again. Something went wrong. There we go. So now we have a toast message as well. Perfect.
And let's see the knowledge base, right? Even I can't even try it, right? Because we protect it with the UI. But if somehow they bypass this, right, they, their API endpoint will still block them because we just added in the files here that they need to have a subscription. One thing I would recommend you do is that you keep track of where you add the subscription check like I did in the files and in the messages here and just double check that it works for premium users right.
Double check that there isn't a bug in this logical If clause or something like that. So in the plugins, where would we add this? I'm not really sure. I wouldn't protect any of this. You should be able to remove your plugin and you should be able to fetch them too, even if you're not pro.
And for secrets, same thing, but we are using upsert. So I'm not sure how smart it is to block the user from updating the secrets because this is a pretty sensitive thing, especially if they want to remove it or something. Vapi, these are only get methods. So again, I don't know. If you want to, you can protect this, but fetching the API, fetching the list of phone numbers and assistance will not occur any costs on your end.
So yeah, and this is, VAPI is bring your own keys anyway. So no costs on your side at all. Widget settings, same things, but then again, we use the upserts. I don't know how smart it is to protect that. Well, you can do it very easily by doing it in the else, right?
So if it's inserting for the first time, this is where you would add the subscription check if you want to protect the widget settings from being updated without a subscription. Amazing, amazing job. So those are the API improvements I wanted us to do. You now know how to protect functions for pro only features and you also know how to refresh the contact session. Let's go ahead and merge this.
32 API improvements. So I'm going to stage all of the changes 32 API improvements. Let's commit. Let me go ahead and open a new branch 32 API improvements and let's publish the branch. Now let's review the pull request.
And here we have the summary. We added automatic extension of active contact sessions to prevent unexpected expiry during conversations. Clear in-app error notification when response enhancement fails. That's the last thing we added with Sonar. We enforced active subscription requirement for file uploads, AI response enhancement, and agent auto replies.
We centralized the session duration configuration for consistent session handling across the backend. And in here we have a few sequence diagrams. This one is explaining our enhanced response and how it works by checking if the subscription is active or throwing an error if it is inactive. This one right here explains how we refresh the contact session ID every time we attempt to create a new conversation. So we call internal system contact sessions refresh.
Same thing if we attempt to create a new message we call contact sessions dot refresh and further we also check if subscriptions get by organization id exists in the very same API endpoint or function and if it does we trigger AI otherwise we just save the message. Exactly what we did and we did a pretty good job here no comments. Let's go ahead and merge this pull request. And once we've merged it, let's go ahead and head back to main and let's click synchronize changes and let's click OK. And Once we've done that, as always, I like to confirm with my graph.
32 API improvements, amazing. That marks the end of this chapter. Amazing job and see you in the next one.