In this chapter, we will develop a widget session authentication. This is basically Auth for anonymous customer using our chat box. And there are many ways to implement this. I chose the session-based anonymous authentication method. This approach will let us collect minimal user information, name and email, and using that we're going to create a unique temporary 24-hour session.
This will allow us to implement low user friction and let them immediately jump into the conversation while still allowing us to maintain conversation continuity. The session which lasts for 24 hours will be able to get refreshed if the user is still active within those 24 hours but if the user is inactive we're going to remove the session for privacy protection. So what are the benefits of this type of authentication? Well, first of all, it is low friction. When you're working within a chat box you want your users to be able to ask a question as soon as possible and I analyzed a lot of existing chat boxes and I was super interested in how they do this and most of them do this very thing right perhaps they don't exactly show an out screen but they let you ask a question and then the chatbot asks you for your email that's the same thing right We are just presenting that as an out screen like that.
But the cool thing is that we don't need any passwords here. Now just don't get it wrong. Email is not an identifier in any way. It is just some extra information. So it's not like you will be able to enter anyone's email and then load their conversations.
No, that's not how it's going to work. That's simply user metadata. So if the user session expires, that's where the operator will be able to contact them to continue the conversation. So those are the benefits of this authentication. What are the cons?
Well, the first con is no email verification as you've just heard, which means that anyone can enter anyone's email address. I did test some other chat boxes and pretty much all of them allow that for the same reason we allow it. It's nothing more than just user metadata so the operator can maybe contact the user on that email address. It is not used as any unique identifier for the user. So it's technically not exactly an issue, but yeah, you have to understand that anyone can enter anyone's email address using this method.
As per technical limitations, sessions can get lost if user clears their local storage or if they switch devices. So we are not protecting against that. And for the User experience issue, well, it will depend on how long you allow the expiration, right? If you make the expiration two hours, users will probably be annoyed that they have to enter their information all the time. So the longer you make your expiration session, the better will user experience be, but more privacy concerns will happen.
Well, more technically security concerns, not exactly privacy concerns. And of course, I don't exactly know where you are and you should abide by your jurisdiction laws about collecting user information because we will be collecting some additional data here like the time zone, the country the user is from and things like that. So you should research that on your own if you're planning to get this to production and make sure you abide by those laws here. Great! So now let's go ahead and see at what we will be doing.
We're going to create the contact session table in our schema. We're going to create contact session functions so we can create a new contact session and we will create the out screen which is essentially a form to create that. And in here I'm going to come to the end of the tutorial and explain how this will all work. We're not going to implement all of this right now, but we will come to this part. So this part will be done.
This local storage and validating the session and checking expiry will be done in some other segments, but I am still giving you the full picture here so you understand it better. Let's start by adding contact session table. So let's go inside of our packages, let's go inside of backend, convex and let's go inside of schema here. So far we only have users. Let's go ahead and go to the top of our schema and let's add contact sessions like that and let's define the table.
Every contact session will have the name as you've just seen on the widget out screen. Then we're going to have the email. We're going to have organization id meaning for what organization is this contact session being created Right? Make sure you don't misspell organization ID here. And we're going to have expiresAt which will be a type of number.
And then we're going to have a metadata object which will be completely optional. And inside we're going to add an object and this object will be able to store as much information as you want for example user agent, language languages Let's make this optional. And it's also a string. Platform the user is on, vendor that the user is using, screen resolution. So basically what I'm doing now is I'm just adding as much things as possible, but you're gonna have to check which things you want to use and which things you are allowed to use depending on where your users will be, of course.
So in here, I'm just teaching you all the things that you are able to add from the user's browser. So I'm just adding here all the things from my source code and we're going to read them now. So user agent language languages which are optional here platform vendor screen resolution viewport size time zone time zone offset cookie enabled referrer and current URL. You can of course limit this to just language if you want to, right? It completely depends on you.
And actually I think it might be a good idea to just make all of them optional, right? Let's assume we can't get all of them, so let's mark all of them as optional. There we go. So what I did is simply wrapped all of my current types into V.optional, like this. So the same thing we did with the metadata and languages was already optional.
So now all of them are optional. I think it just makes sense, right? You don't know what we are going what you're going to be able to capture. You don't know what's available. Maybe user is using some super weird browser.
So I think this actually makes more sense. And let's go ahead now and let's create an index here. So we're creating an index on the entire context session table here. So let's go ahead and let's add dot index by expires at and simply target the field expires at like that. So now we can very quickly query by expires at if we need it and actually think we can add one more index here by organization ID organization ID.
We're going to see if we need this or not. I think we might actually need it if we don't thankfully it's super easy to remove an index in convex you just remove it here that's it. Once you've done that make sure you run turbo dev in the root of your application so that you have your back end running and you can see that that is going to check for indexes and you can see it added two indexes and it validated the schema so that's how you know you did everything correctly. If you're getting any errors you did something incorrectly so please double check with the code here. Perfect.
Now let's go ahead and let's create a simple create function for this contact session. So we're going to do that here instead of the convex folder and previously I taught you that you can just create you know users.ts and write the functions in here and sure you can do that but I think it's time for us to create a structure that we're going to follow in this tutorial. The structure that I chose is to create a folder called public and then we're going to have a folder called private and one more folder called system. And the reason I chose that structure is because some of our convex functions will be publicly callable from the public widget component. So I want to semantically separate them in that folder so that I know whatever I write inside of this public folder should be considered accessible by anyone that's why I'm doing that and that's why I think you should do it too so that you have all of your risky functions in one place.
So let's go ahead and create contact sessions inside. Now in here let's go ahead and let's import V from convex values. Let's import mutation from generated server. And let's export const create mutation. Let's go ahead and open the arguments we're going to accept name we're going to accept email, we're going to accept organization id, and we're going to accept metadata.
So Inside of the metadata here we need to pass an object and inside of here we basically have to add all of our items. So what you can do is you can just go back inside of the schema and you can just copy all of them like that. So after you have written your arguments go ahead and add the handler which is going to be an asynchronous method. Like that. And the second are the second parameter are the actual arguments here.
So now let's go ahead and let's define the session duration. How long will this session last. So I'm going to go ahead and do a constant session duration and I'm going to make it 24 hours. But in milliseconds, So let's add a comment. 24 hours in milliseconds.
Or we can just use this so we don't have to write unnecessary comments. There we go. And now let's go ahead and use that to create our expires at so let's get the variable now to be date dot now and then let's define expires at to be now plus session duration in milliseconds. Let's define const contactSessionId to be awaitContext.database insert into contactSessions So yes, convex database insert will always return ID. That's why I named this ID in advance because I knew that convexes insert always returns back the ID that was created.
Let's go ahead and let's add arguments.name, email, arguments.email, organization ID, arguments organization ID, expires at, metadata, arguments, metadata. Like that. And let's return contact session ID. There we go. So make sure we have this saved and let's check if our functions are ready So you can see whenever you have any errors in your code, the convex backend will not compile.
So make sure that all of them is correct. So you will see the success message here. Perfect. So I think that this is okay to be passed like this. I guess, we'll see.
Now let's go ahead and let's actually create the widget auth screen so for that we're going to go inside of apps widget modules widget ui and let's actually create a new folder called screens like this and in here let's go ahead and let's create widget.out.screen.dsx and in here we're going to import workspace header my apologies, widget header, not workspace header We're not going to use the footer so no need for that. Let's just export const widget out screen for now. And let's go ahead and return an empty fragment widget header and instead of the widget header we can actually copy what is currently inside of our widget app page widget view there we go So you can copy this entire widget header and you can actually cut it from the widget view and then put it in the widget out screen. And in here, Instead of how can we help you today, let's indicate that this is kind of the out screen. So let's say, let's get you started.
And you can replace this with AppOS, like that. So let's get you started, like that. And let's go inside of the widget view and instead of this I am just going to render the widget out screen and for now you can comment out the widget footer and you can comment out these two imports because we're not using them right now. So widget out screen. Make sure that you have your widget running and head to localhost 3001 and this should load the new let's get you started with no footer so there should be no footer because we just commented it out and we are only looking at the widget out screen And now that we have this ready we have to import SORM form components.
So in the previous or the chapter before the previous chapter I think we added all components from Shazian to our project. So now let's go ahead and let's add them. Let me just replace this. I really like to use modules, widget, UI. I really like this.
Okay, let's import from workspace UI components form, form message, item field, control and form. Then let's also import the button. Let's go ahead and import the input. And let's now import use form from react-hook form. If you're wondering where do we get this from, well, this should be installed packages UI package json I think react hook form let me check yeah you can see react hook form exists here but the reason we are getting errors is because it also needs to be installed in the app that we are using it.
So we're going to have to be careful here. We're getting to and I think the same is true for Zod which we also need. Yes. So if you check the package.json of your package's UI, Zod also exists here. So I think we're going to have to be careful and try and install the same versions here.
And let me just check. There is one more thing that we need and that is hook form resolvers Zod. Now when I try to install these packages the first time in my original source code I had a lot of bugs a lot of mismatches so we're going to go ahead and try and do this together now. So this is how I'm going to attempt it. I'm going to find the version of Zod that I need and I'm going to shut down my app.
In the root of my app I'm going to do pnpm filter widget right this is for widget and I'm going to attempt to add the exact same one so add zod and let me just see can I do this okay so zod at And I'm not sure if I can do this when I'm adding it like into the terminal like that? So let me try and add it here and see if this will give me this exact version or not. So first things first this should now disappear. Great. But let me check exactly what version was that.
Looks like that exact version was added. So that's great. Look is it. I guess this is how you do it then. Obviously if you have any recommendation for me of how I should enhance using BNPM and these versions please write it in the comments or email me whatever I would love to learn more.
So now let's do the same thing for React hook form. Let's go ahead and do same thing. So. React hook form at 761.1. Of course, you use the I mean, you should have the same version as me, but just go inside of your packages UI package.
Jason, Let me show you where that is. Inside of your packages, UI package.json here and just find the version. So make sure you're adding this to the widget. Let me check if that is correct now. So I'm going to go here and check react hook form.
I think that's the exact version. Perfect. And we also have to do it for hook form and this hook form was giving me a lot of problems. So we're going to see now widget add hook form resolvers five to zero. So hook form resolvers making sure this is the one and we're going to see.
Yeah basically for me this hook form resolvers was expecting a completely different version of Zod and it just caused such a weird mismatch. I think this is because Zod is now getting version 4 and everything is just bundled a little bit. So let me try this again. Instead of my package.json for the widget, I now have hook form resolvers, react hook form and Zod all seemingly correct versions. Let's see.
Let's go ahead and try doing turbo dev. If it doesn't work, well, we're going to debug. Looks like it's okay, but let's wait until we actually add some components to see if it's okay or not. So back instead of the widget modules, UI screens, widget out screen. Perfect.
No more errors here. That's great. Now let's go ahead and let's quickly define the form schema for this out screen so you can do that by using z.object and adding the name with a minimum requirement of one and name is required and email z.string.email invalid email address. Form schema is the constant, perfect and then inside of your widget out screen here what you're going to do is you're going to define constant form to be useForm which we import from React hook form and inside open this type annotation and pass in z as Zod.Infer type of form schema constant which we just defined above. Then go ahead and execute the use form hook and pass in an object inside with a resolver ZodResolver using again form schema and default values for our two fields.
Perfect. And now I'm going to create const on submit method here which is going to be an asynchronous method and the values here will be z.infer type of form schema. And inside we can just console log the values for now. Perfect. Now let's go ahead and let's go below the widget header here and let's add form Let's go ahead and spread form inside Then let's add native form element Let's give it class name flex flex1 flexcolumn gap y4 and padding 4 Let's give it on submit here to be form handle submit on submit like this.
And inside let's do form field and let's give it control form dot control. Name will be name, render will destruct the field like so and render form item. So this composition, if you're getting confused, like how do I know this? This is just chat-cn way of writing forms. Nothing special, right?
I just learned this composition because I've done it a million times. You can find it in chat scene documentation, just type form. Inside of each form item, there is form control and then you render the type of component that will be controlled in this form. In our case, that is just a simple input. Let's give this a class name, height 10, and BG background.
Placeholder will be, for example, John Doe. Type will be text. I always like to explicitly add my types even though this is the default. And let's go ahead and spread the field property. The field property basically has all the useful, you can check it.
If you're going to the form field, controller props, somewhere you can find it. Basically, it will give your input on change, on blur, on focus, all of those useful things. Okay, so I've saved this. And now let's try and visit this page. Let's refresh.
Let's see. Are we getting any errors? And there we go. We are getting some errors. So we do have to fix them.
All right. The good news is I've already encountered this before, so I know it's fixable, but I will have to pause the video a bit and I'm going to try and find the exact discussion that helped me fix this issue. All right so first of all here is what I googled. So I put module not found can't resolve zod v4 core and then I added hook form because well that's the error we are getting and I can see that it's coming from hook form resolvers because if you just Google without hook form it will give you some similar issues but they're not exactly associated with our problem. Our problem is within hook form and I found this github issue here which seems to describe our exact problems.
So pnpm and hook form resolvers that is pretty much the exact thing that's happening to us here. Now in here there are several fixes so this person here seems to suggest to check the installed Zod version and if it's below 325.0 upgrade to the latest Zod 4 release. After upgrading, delete node modules and the log file and then reinstall the appendices. And if you're still seeing issues, you can try setting this module resolution in tsconfig.json. Another person here says that all they had to do was pnpm remove Zod and then pnpm add Zod 4.0.5, two weeks ago, so it seems promising.
But here's what I found. This was also two weeks ago. In here you can see that I actually marked this for myself. I added some emojis because this is what obviously helped me in my project. What this person did is they downgraded back to a working version of Zod.
So in our case let's see what is our Zod version. So it's 3.24.2 and they're using 3.25.67. So what if we went ahead and modified our Zod version to this one first. Let's go ahead and try that. Or actually, you know, we can also try doing this.
We can try pnpm remove Zod. Let's try that. So I think this is a good exercise to try and see how we can resolve this. So let's first try PMPM remove Zod. Because you know if we can use the newer version why not.
Okay so I can't just do this directly in my root file. I assume I have to filter to a specific place where I'm using it. Let's see. What if I changed to this Zod version in all of my places. So what if I did this?
Change it here and in my package's UI I also changed it to that version. And what if I then run pnpm install in the root of my app? So that should now modify those two packages. And then what they did is they removed node modules and PMPM lock file. Should we do that as well?
Let's go inside of apps. And let's consider widget because that's where we just added this. Let's do remove node modules. Okay. Rmrf node modules.
And they also mention removing pnpm log file. Not too happy about that but... Do I have? Let me just see. Do I even have that file there?
I don't think I have that file inside of my widget. Yes, I do not have it. So let's just do rmrf node modules and let's go ahead outside of our app to the root, pnpm install again. Okay, let's try TurboDev now. Focusing on the widget, this is the one we just modified.
Let's try refreshing localhost 3001 And it seems to be fixed. Seems to be working. This is obviously how I fix it in my initial project. Now, I hope you managed to fix it as well, because it looks like you will certainly come across this issue. So what I'm actually going to do is I'm going to, well, I mean, you can, I just told you how to find this issue, but basically I suggest that you try any of these solutions that they're giving you here?
But I think that this person has this most similar example to ours here. So he's using HookForm Resolvers version 5. He's using a similar version like us. Looks like his is a little bit upgraded. I mean, theirs.
So I think this shouldn't be working because they're using PNPM and they're using TurboPack. Ours seems to be working fine. What I would suggest now after just trying this, also try turbo build. So just make sure that this didn't break the build. If the build passes, I think that's almost like, well, ours just failed.
So let's see why. Because of type, perfect. That's actually perfect. Let's just quickly check this. So, sorry for being so over this.
I just want to make sure that you can follow along. I don't want you to be stuck with an error because this is something new here that we are doing. Let's go inside of components, widget footer here and let's fix this by just let's just do this. It's easier to check. OK let's do turbo build.
I just want to see is the build still failing because of this hook form or is it all good I think we are all good if it's past the linting test there we go so it's still building the web So let's just wait for the web to see what's going to happen here because I want all three of them to pass. All right, here we go. All three builds successful. So I think That is as much proof as we can get that we fixed our issue. If the development is working, if TurboBuild is working, I have to assume that we fixed the issue.
So we're going to probably have to revisit this problem very soon when we add the same packages to web, right? So let me just quickly check. Our Zod version everywhere is 3.25.67, resolvers 5.2.0, and that seems to be okay. So I think this is good. I think we can now go ahead and continue developing.
Perfect. So let's go back inside of widget out screen here. And we just added the first input here. Now let's go ahead and let's add the second input. So you can just copy this entire form field and just paste it below.
And this one will control the email field. And for example, put John Doe example.com. And the type here will be email type. And you can also add form message which is a self-closing tag beneath each form control. And what this will do is simply render any error if it exists.
Now, outside of this self-closing tag, add a button with continue text. And let's go ahead and give it a disabled prop. If form, form state is submitting, whoops. Size is large and type is submit all right let's refresh oops yeah make sure you have turbo dev running and head to localhost 3001 and you should see your fields here. Perfect.
Now, let's go ahead and let's add, let's first test the submit method, right? So if you just press continue, you can see errors everywhere, but if you try test, if you try test, here it tells you it needs to be an address. And when you hit continue, there we go. We have the values, email and name. Perfect.
So now what we're going to do is we're going to go ahead and create the mutation to store our contact session. So const create contact session will be use mutation from convex react package. And go ahead and add API from workspace backend generated API API dot public dot contact sessions dot create that's the one we need. Excellent and the first thing I'm going to do here is I'm going to check if there is no organization ID. Because we need the organization ID.
Yeah slight problem here. Is that we are not passing the organization ID here and we are not going to pass it as a prop. Actually, we are going to pass it through our state management. So for now, just for now, I'm going to do const organization ID 123 and add a comment const organization ID one to three and add a comment temporary test organization ID before we add state management. All right.
So I'm going to continue writing the code as you normally would. If there is no organization ID, I'm going to break the function because we have to make sure that organization ID is present. And now we're going to create our metadata object. So this is where we basically add all of those fields. So user agent can be accessed to navigator.useragent.
Language, well, language can be accessed through navigator.language like that. Languages are an optional array. So you have to join it with a comma like this. Now you can access the platform, but you can see it's deprecated. Even though I was able to extract the platform, as I said, you don't have to track all of these things from a user.
I'm just giving you some interesting information that you can do here. So vendor, screen resolution, you can take the screen width and the Screen height, viewport size, same thing. So these things are sometimes useful to debug, to help debug your user, like are they on mobile or are they on desktop, right? Time zone, use ENTL, date format, resolved options, time zone. Then the time zone offset, new date, get time zone offset, whether the cookies are enabled for the user.
And let's go ahead and try the referrer or fall back to direct and the current URL. And I think that you might actually be able to give this a type of document, contact sessions metadata like this. So if you try adding something like this, it will give you an error. And yes, you can import the doc from doc workspace back and generated data model. I think this is a little bit safer because it will warn you early if something's incorrect here so make sure that it matches your schema metadata.
Excellent and once you have that you are ready to get the Contact Session ID through await create contact session and passing, I think we can just spread the values and then metadata and organization ID. I think that is it. Perfect. So now after this, what we're going to do is just console.log the contact session ID. That's the only thing we're going to do for now.
And let's also prepare our convex dev simply so that we can actually check in the database is it created or not. So instead of my echo tutorial here. Instead of my data contact sessions there we go It's currently empty. So I'm going to go ahead and create Antonio Antonio at example.com continue and we have the contact session back. Perfect.
And in here We have the email, we have the metadata for the user. You can see all of that information about where I come from, all of those things, perfect. And organization ID, which is just a mock, as well as when this will expire, right? So you can see that this will expire in 24 hours which according to my date is correct. Perfect so that obviously works so in the next chapter we're going to implement the actual while saving this into the local storage and the state management and maybe redirecting the screens so we can initiate a conversation finally.
Perfect. So let's go ahead now and let's mark this as completed. So we added a table, the functions and created the widget out screen and now let's go ahead and push this to github so 10 widget session. Let me just go ahead close this, add all changes, 10 widget session, let's commit, let me open a new branch, 10 widget session and I'm going to publish the branch. Then I'm going to go inside of my echo tutorial repository here and I'm going to compare and pull request and let's see what CodeRabbit has to say about this change.
And here we have the summary by CodeRabbit. We introduced a user authentication form that collects name, email, and browser metadata with validation and back-end submission. We also added back-end support for creating set contact sessions including a new database table and mutation for storing user and metadata information. So what I find very cool about CodeRabbit here is that its sequence diagram here looks so familiar, almost like the one I presented to you in the beginning, right? So This is the one that I manually created.
So I have some more information because I know how it will behave in the future, right? But it's super impressive how CodeRabbit itself managed to recreate that without ever seeing that diagram. That's exactly what we do. We submit form name and email in the widget out screen. We validate the input via Zod.
We collect browser metadata and we then submit that form data with the metadata and then we insert the contact session record and bring back the session ID. Perfect. So in here it left a few comments, all completely valid, but some we know are temporary like this organization ID. This is completely temporary. We are going to change this in a different way.
We are not going to be passing props here. In here, definitely a good suggestion, it recommends handling this instead of try and catch to log some errors. We're going to do that in the next chapter, we're going to add some error logging. In here it suggests validating instead of type vstring to use vemail. The problem is this is in the convex backend so I have to research if they have validators for email or not.
Definitely good suggestion. And another very good suggestion here we should always validate if the organization ID that we are passing and creating the contact session into actually exists. This is a little bit problematic because organizations are handled externally. So we're going to see how we're going to do that in my original tutorial. I don't think I've validated that but very very good idea to do this.
When I say my original tutorial I meant my original source code. Excellent. Very good suggestions. I will keep all of them in mind, especially the error handling and organization validation. Perfect.
Now let's go ahead and let's go inside of our main branch. Let's synchronize our changes here. Perfect. And once they've been synchronized, I always like to revisit the graph to confirm. We detached in the chapter 10 and we merged it back into our app.
I believe that that marks the end of this chapter. We successfully pushed to commit, we've committed the changes, pushed them and reviewed the PR. Amazing job and see you in the next chapter!