Build a Multi-Tenant B2B App With BUN and HTMX - FULL COURSE

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
just around two months ago now I created the BET stack that's bun Alicia turso and htmx and in that first video I only made a really simple to-do app but I want to show that that technology can scale to something a bit more complex so in this video we're going to make a multi-tenant B2B SAS app start to finish and I'm going to show you the whole thing so let's take a sneak peek of what the end product is going to look like we're going to be building a customer service ticket app where every organization has their own entire database and each organization is going to have their own custom link they can send to customers so it's going to look like this and a customer can enter in I have a link problem it doesn't work sometimes and they're going to be able to create that ticket and this is actually the employee view but they're going to be able to go in and be like I can help you and then let's say they never respond you can mark it as closed and because this is multi-tenant right to organization scoped we want to add employees so we have this employee invite code we can copy and then if we go to a new user who's never signed in before we can sign in and get directed to this new user page where we can either create a new organization that's going to create an entire new database for that organization or we can join an existing organization so here we can put in our code and now we're a member of org1 and see all the tickets and close them very cool I put a ton of thought into all the different parts of the stack and I think the way they come together is something really cohesive and I hope you'll see that in the rest of the video and make sure to stick around to the end because I'm going to show you how we can scale our app all around the world at the edge in just a couple of terminal commands without the way let's get straight into it so to get started we're going to use the create Beth app CLI it's a bit Bare Bones for now I'm hoping to make it more like T3 app esque in the future where you're gonna have options you can choose but it works for now so I'm just in a fresh new directory on my WSL machine so I'm going to type in button X create Beth app and you should be good to do at latest here my the bun package manager are some weird caching issues so I'm going to manually type in the version it might be updated past this by the time you see this video so just stick to that latest you should be fine so what this is going to do is again right now very very simple it's just basically cloning a template I have set up so you're probably going to want to uh get rid of that git repository and put in our own then we could just hear and commit so this template is kind of big uh but I'm going to take some time to just explain what's going on first and then we'll start building the app so the project the create Beth app CLI creates might look kind of big but I'm just gonna go through it folder by folder just kind of explain what's going on before we start so at the very top is our auth we're using Lucia auth which is a really really great auth Library kind of takes away a lot of the complexity um of auth and gives you a bunch of functions to very easily call stuff and they also have a lot of built-in oauth providers which we're definitely going to be taking advantage of then in our components folder we have our standard jsx components you might recognize this from the first video this is just our like base HTML shell and something new I've added since then is the ability to have async components so these are kind of like server components if you worked with react so here you can see we have this async initial tweet list and in this function it's actually calling the DB to get its own data I'm not going to be doing a ton of these and the app we're going to build later but I actually have full like react cache and next cash equivalents if you really want to get into this but I'm probably going to make another video about that so look out for that something you'll notice here that's also kind of cool is a type error so we actually have type safety on our hdmax attributes and that's from this htmx.d.ts file and so basically because Alicia is typesafe we can get all like the get routes and all the post routes then we can assign all the ones that start with API to the like HX post HX skit which creates a really cool DX now this one's bad because we added some search Ram so might be some room to approve there but very very cool um moving on to config this is mainly just for environment variables so we're using the end package from T3 which is really really awesome allows us to Zod validate our environment variables and here we just have some database connection stuffs log level stuff node environment and our oauth stuff pretty pretty standard I also added the ability for you to add command line arcs I never really found a use for that but it's there if you need now our contacts this is pretty important because the way Alicia works is you can have routers that like inherent behavior from other routers so we're going to have a global context router that all of our other routers are going to inherit from and then we're going to merge all those routers back into one single router which we're gonna then like that's the one we call Dot listen on which is going to actually open uh the server which is pretty cool um and you might be worried about uh using this router all over the place but Alicia is actually again this happens like at start time so it's not like during requests and at least if you provide a unique name which we are automatically deduplicates um when you use routers multiple times so here we're just uh decorating which just means kind of adding an object to our contacts we're adding our DB our config our auth and we're adding some bet stack stuff and some logging um now under the controller so here is where our API routes are um so you can see we export this API router with a API prefix and we then use and this is like I said where we are merging the router so here if we go to our auth controller so this is a new router that inherents like the context router and then adds some routes then we create this API which inherits that auth controller and then we're going to merge that with our other routes to create our main router so our auth controller has some sign ups some sign in some sign out again using Lucia and here we have our our tweets controller which does some crud for the tweets for this example app moving on to our DB now we're using drizzle of course a really awesome typesafe orm and our database provider is turso uh who's actually sponsoring this video they have some really really amazing features we're going to take advantage of in our app later in the video but just in general they're a really great uh sqlite for the edge provided so now onto our Pages um and this is kind of file system routed now you don't have to do it this way and you can see actually like we don't do file system routing for uh our API routes right like we have a post endpoint and another endpoint in the same file um but for pages I kind of liked uh that um pages are like actual routes the user will see and so it could be kind of uh helpful to have like a folder directory that represents those routes that the user can see where the API routes you know they can be a bit more condensed they might be smaller um but for routes the users can see I like the idea of doing kind of file based s routing it's not actually file based you can see for like we have an auth group here and we have our login route um and you can see actually like you don't have to do file based routing right like we I have um the GitHub callback in this file um and then we again merging the group so we merge this and any other routes we want to in this folder into this auth group and then we merge that auth group into this kind of top level uh collection route which we call pages and that also has our our index route in here um and again here's our context so we're using the context and then here we can like validate our request with our contacts and return the session and then use that session in our Handler um finally the types so htmx uh kind of already talked about that we have some cool hmx typing and Lucia this is just some types for Lucia and finally where we bring it all together into our main.ts file so we're using the static plug-in and this is to serve like a public folder this is really going to be only used for CSS but um pretty cool and for CSS we're using Uno CSS and Uno CSS is really really awesome um it's super fast and has a lot of really cool features the biggest really being uh the icons so it provides pure CSS icons so it's literally like a class name on a div converts it into an icon which is super awesome we're going to use those a ton um and it creates I have the out directory it has public dist Uno CSS so um we have that in our base uh HTML it's just fetching that and that's being served by the static plugin plugin and then we're using our API routes we're using our Pages routes and that's going to give us one big router that represents our entire app which we're going to call it out listen on so let's discuss a bit about the kind of overall structure for this app we're going to build so like I mentioned it's multi-tenant so we're going to have multiple organizations which all have like their own data and typically you know you could represent that in one database but we might want to separate that data right maybe it's super sensitive but also we might want to make it super easy to extract right like if one organization wants all their data it'd be really cool if we could just like dump the contents of one entire database now normally databases are really expensive but remember SQL Lite is just a file so really we can make a ton of sqlite databases super cheap and that's actually what we're going to be able to do now with turso so turso is going to allow you to create up to 500 databases on their free plan and it's going to be up to 10 000 on their scalar plan and so we're going to create a new database for every organization and all the ticket information and all the chat information is going to be stored completely separately so if that organization ever wanted that data we could just sqlite dump it into a single file and give it to them super super easy which is really cool the other thing we're going to take advantage of is turso's new embedded replicas feature so you might have seen this video from Theo about like Edge latency and it kind of demonstrates one of the problems with Edge where it's really great to have your server right next to your user but if you have like one big database in a single data center kind of multiple round trips there can add a ton of latency and here's a copy of that diagram no terso already makes it super easy by basically allowing you to add a replica here right so here's like the primary and here the database uh the replica is talking to the primary and this allows you to bring this database much closer right but there's still a network Bounder here like even if this is you know 40 milliseconds versus like 200 milliseconds um sqlite is an embedded database right like we could get even faster if we could read a file and so what turso's new feature allows you to do is to put a replica like inside your computer it's actually a sqlite file and what this means is you can get microsecond reads while still keeping up your entire replicated Global Network and this is important because let's say you have like another Edge function over here um you can still talk to the primary right like you're not limited to this being on this system uh because all the like the primary is still remote but it means that when you have these like sequential reads they're just super super fast which is really really cool and we're going to take advantage of that uh so now let's actually get to coding so the first thing I'm going to do is make a new database with turso with tursodb create and I'm going to call this one I call this Beth Beth let's go um and so then meanwhile let's go in I'm gonna create hindu.env file and when this finishes um we're just going to leave it in one location for now and so we're gonna torso DB show Beth dash dash URL to get the URL I'm gonna put that into our environment and we're going to terso DB tokens create Beth to get our authogan I'm just going to copy paste that in um and make sure these are are kept secret I'm definitely gonna have these all rolled by the time you see this video so don't even uh don't try to connect because they're all going to be gone um so now we're going to add make our node environment development and our log level I'm just going to set that to debug for now um and these I think we can just put in whatever and our app will still launch so if we bun Dev now oh is it mad what's it mad about let's see so here this is actually something very nice that it will tell us if we're missing an environment variable so it's we are missing our database connection type now this is actually really important because again uh working with turso you get a ton of flexibility so you can have a local sqlite file for development you can have a remote database or you could have a local replica database which is what we're going to talk about later so for now I'm just going to put this in as remote and we can start up our server again and now it should be launching so if we go here and we go to localhost let's see ah because we forgot to DB push so we're gonna bun DB push and we're going to click yes and changes applied so then we can bun Dev again if we go back here looks good so we can sign in and here so um in the example app I have a uh so we can just test one two three and we can sign up oh sign up right and now we can create a new tweet I'm gonna say hi uh this is a tweet cool all right so let's actually now we're gonna gut a lot of this stuff to get started on our app right because the Twitter clone you can play around with this but we're not making just a Twitter clone so let's get started um the first thing we're going to do is we're going to switch to Google auth instead of GitHub auth um you know GitHub is great but I think uh for this app you know I feel like businesses everyone has a Google account so we're gonna work with Google so let's start by we're gonna shut down our server and we're gonna go to auth we're gonna instead of GitHub we're gonna import Google from the Lucio providers we're just gonna put that in here and then we're going to change our config variables to let's make these Google client ID Google client secret now let's see what it's asking for client ID client secret and it's asking I think it wants a callback uh URL right so we're going to make this [Music] um let's actually to our config let's add a hostname as well host aim our host URL that works and so we're going to put in config.hostforl.org Google and we're actually going to make this slash API as well so API auth Google slash callback that works and here we get uh Google and Google client secret looks pretty good um forgot the dollar sign there and so now we want to update our auth to match our config so to host URL roast URL we're going to add this looks good and then now we need to make these Google and now let's go to Google and make a new app so we're going to go to Google Cloud credentials so you can see here's the one I have set up but we're actually going to make a new project we're going to call this Beth SAS let's go that's SAS been good I can take just a second to load so once it loads we're going to need to configure our consent screen so um we're going to make this external because we want other people to use it we'll call this uh Beth sass let's go I'm just going to put in my email here um and then the authorized domains we're just going to put uh what our production domain is going to be and we deploy and for Scopes we're going to add the email scope because we want to have the user's email I think I need to click save somewhere yep update looks good we're gonna we're gonna add some test users we're going to add um now when you if you ever do go to production I think you have to get your app verified but just for now I'm just going to add two emails of mine that we can use to test looks good and so we can go back to the dashboard and we can get our credentials so let's make a new create credentials oauth client ID and we're going to make this a web application read all this just web app one this is going to be where we put our HTTP localhost 3000 then our callback URL is what we're gonna is what we had earlier here so we're gonna hit that from here and go back and just add that here it looks good and here so now we can go to our environment variables and put these in so it's our client ID is our client Secret looks good so now we can okay so now we're also going to want to update our database schema to kind of hold this additional information we're getting from Google so I'm going to rename this handle to name and then we're also going to add a email field and this actually could be knowable because if we look at the Google user we get from Lucia you'll see the email is actually optional so what we're going to use we're going to use name we're going to use the picture we're going to use the email as well so we can go back here name and then picture it's going to be text this one is going to be not no looks good so now that we change our schema we're going to want to push those changes now now because we had some data in there before that doesn't necessarily match the schema I'm just going to delete it for now if you have like actual data you care about I definitely think about how you want to migrate that but just for now I'm just going to turn so DB shell call that Beth right it's going to put us in and so we're just going to delete from uh user a key we're going to delete from user session never going to delete from user looks good so if we got tables um you'll see that we have all those but they're just going to be empty now so we can D and then we can push our database schema add that to column looks good awesome so now we're going to change our auth controller a little bit so in the template example it uses kind of a username password signup but we actually don't want any of that so we're just gonna get rid of it looks good um so we're going to use our sign out uh so here's our sign out route and all sign out is doing is it's taking the context it's validating it and if there's a session it's going to invalidate that session and then just kind of set the cookie to null so something you'll notice here is this setting the HX location header this kind of looks like setting the normal location header which is how you would do a redirect but this is for something htmx does where it can actually instead of doing like a full page redirect htmx will actually get that link for the redirect in the background and kind of do a client-side swap which can be a little bit smoother for the user and it's something we definitely want to take advantage of but if htmx isn't the one that made that request it's actually not going to work so what we want to do is only set this HX location header if it's an htmx request otherwise we just want to do a normal redirect so to do that we're going to create a little custom function and we're going to do that we're going to create a new folder called lib create new file just a next.ts file we're going to export function redirect and this is going to take a set and a headers and these are objects we're going to get from our Alicia context and we'll worry about the types in a second uh let's get rid of that so now um what we're going to do is if headers at each X request equal to true and this is just a header that htmx puts out every request so we can know if it came from htmx or not and so if it's so we're going to set dot headers and we're going to put we're going to put HX location actually we need to take a href as well here each ref string or set that equal to href otherwise else we're going to set that redirect um let's do hre so now the type of this so headers is pretty easy it's just a record string string and this actually string or no and again these are things we get from the Alicia context so I'll show you that in a second here so if we go to Alicia on this context object we get context dot um context.headers again uh record Stringer null and also context.set which is this type and so um you can see those headers this is like where you can set the headers for the response and status is either a number or we have like all the strings which are like the text representations of those Summers um so we can just copy this here and put that in set number or string and that looks good actually this is also redirect sure perfect so now we're going to replace this with redirect from slash lib and we're going to put in set context.set headers contact stat headers and then we're going to set our href2 slash we're actually going to make uh sign out a get request as well and what this is going to do is if we tend to get request sign out we're going to handle that auth request see if there's a session um and if there's a session we're going to invalidate it and redirect the user to um to the index pageant and so if there's a session we're going to invalidate it and redirect the user to the index page we're actually also going to do that um in here if there's not a session either right so either way just always getting redirected to the index page um actually we can just put a early return there just for typescript looking good okay so now we need to add how to sign in so now we're going to go back to our auth uh our auth file here and this is still called GitHub author we're going to call this Google auth um this is going to provide us all the kind of ways to work with the Google auth so we're going to create a new endpoint it's going to be slash git it's going to be slash sign in Google and this doesn't have to be such Google but you know we might want something else in the future and just we're going to create a new Handler here um so what we want to do is we're going to import our Google auth object and we're going to kind of just uh result right to Google auth Dot and we're going to generate the authorization URL and so this result is actually a URL and a state so we're going to destructure that I'm going to URL and State looks good so now we're going to create a cookie so we're going to con state cookie equals serialized cookie we're going to get that from Lucio we're going to call this the Google auth State it's going to take our state and some an object of options we're going to set the max age to say 60 times 60 then we're going to set HTTP only to true now for the secure option secure we're going to set that to whether our config node end so we're going to get our config.m DOT node and there we go is equal to production otherwise you know we don't need to be super secure and our path is just going to be the index crop and uh it looks like I forgot an await here we also need to make this an async function looks good so now that we have our cookie we're gonna get our set from our context set we're going to set that header set cookie we actually don't need to serialize this at all even there we go they're going to set dot redirect to the URL this actually uh is a URL object so we need to call tostring looks good so now this is not a function this is a property looks good so the final thing we need to do before we get our auth working is set up our callback URL right so that's where we set um Google to point out that slash API auth Google callback so let's go set that up so we're going to create a new endpoint slash uh Google slash callback and so what uh Google is going to do is it's going to set some query parameters it's also going to set um some cookies which we're going to access through the headers and then we're going to set up our Handler so the first thing we need to do is get What's called the code and the state from the query parameter so we're going to destructure those state code equals query so now we need to get the cookie and so first we're going to get all the cookies and that's going to be we're going to use the parse cookie function from Lucia and we're going to pass it our headers at cookie or an empty string looks good and so we're going to get our state cookie equals our Google auth State perfect so now that we have these there's a couple things we need to guard for so first of all if we don't have our state cookie that's that's going to be an issue now if we also don't have our state that's also going to be an issue and if these aren't the same state cookie equal to State that's also going to be a problem and the final thing is we also need the code so or add code and if that happens we're just going to set the status equal to unauthorized and we're going to return looks good so now we can proceed uh in Lucia this is just from their their documentation they recommend throwing this all in a try catch box we're going to do uh that so the first thing we're going to do is we're going to get a result we're going to get our Google auth that we're going to validate validate callback we're gonna put in our code so from this object we get back we're going to get our create user create existing user and Google user perfect so now we're going to create a little helper function called git user it's going to be async the first thing we're going to do is thank you copilot get the existing user and if there's an existing user we're just going to create it otherwise we're going to create a user now what do we need to create a user so to create a user these are all the columns in our database so we're going to fill it and we can see I'm going to ask for some attributes and it wants a handle and that's because we have not updated our Lucia types to match our database basically whenever we make a change to our database auth schema I need to update our Lucia types so in our auth we have a name a optional email and a non-optional picture so we're gonna update that we're gonna have name picture string and email that's all right email perfect now when we go back here we have a little error here and we'll get to that in a second and there's one other error here looks like I was uh that's not supposed to be like that so now we're going to provide these attributes it's asking for the name and we're going to get the Google user.name loser.email and picture well user.picture looks good are the types happy um the little issue it's saying is it expects email to be optional or there and so we can actually just all we need to do let's make this string or undefined or no right because it could be no on the database so I'm going to go here and we're just going to make this no perfect and then of course don't forget a comma so now we're going to call const user equals await get user perfect and now that we have a user we're going to create a sessions we're going to cons session equals and we're going to get the auth object from our context auth then we're going to auth.create session and copilot's a little wrong here we're going to pass in our user ID user Dot ID looks good oh not good attributes we need to put as a undefined object empty object and this should be user ID so this attributes field is if you want to have like custom attributes for each session you can put those in your database schema put them in the Lucio types for our use case we won't actually need this so we can just leave it as empty and now we get a promise to a session so we're going to await that now that we have a session we're going to get a cookie session cookie that's all right session cookie we're just going to pass out our session and finally now that we have our cookie we're going to set dot headers.cookie to the new cookie we're going to call our redirect function we're going to redirect them um just to slash for now and again we need to pass in our set and headers perfect so if an error occurs what are we going to do so we're going to import uh the Not Duplicate error if it's an instance of oauth request error this is something we get from Lucia so if this is the case we're just going to put unauthorized and then otherwise um we're just probably going to return a 500. so we're just going to set that status internal server error and uh return now the other thing we can do is we can get our log from here again this is a Pinot logger and what's uh let's log out this error I'm gonna log dot error E and say error signing in with Google perfect so now let's update our main auth object so basically this user schema is what's represented in our database it is get user attributes is what's going to be like available in our auth um context so we no longer have a handle but we do have is perfect now what else is on data let's see it's like Foo data Dot um ID that might be useful so we're just going to throw that on there as well perfect so now we can go to our index page um and see let's put in the name that name and so we're getting the session and this derived so derive is something that basically allows you to modify your Alicia context but it runs every request so every request before we get to the Handler derived runs and it's taking our contacts and it's getting the session and if there's a session it's basically adding it into the context so this session here is this session here and so then in our component say if there's a session we can say hi and this sign out this should be good by zumad let's see that's API auth sign up that should be okay just restart the typescript server maybe it's mad and otherwise we're going to go to slash login perfect let's see what's happening so we can see Slash API also sign out this is a post yeah that's why here we want to make this actually an anchor and this is going to be the href perfect so now um in this initial tweet list this one's not going to work anymore so we're just going to get rid of this so let's try this out so let's go to bond Dev see if there's any errors kit hub off or did I forget to remove ah here I think other than that we should be good so this is our sign up page our login page let's let's make sure we're using the the right terms for that so on the back end we have slash sign in slash Google so let's just make this slash login slash Google then we're going to have a signed in page which this is going to be slash login so we need this to log in okay and then um here we're doing the same thing so if there's a session we're just redirecting to slash and then let's see here actually get rid of this form we don't need because this is for the email password stuff get rid of this get rid of this and here we're going to put the Plus login slash Google let's say sign in with Google and then this this is the Uno CSS icons I was talking about earlier I'm just going to make this the Google icon I think there we should be good and we can get rid of all this and let's try it out oh there we go okay so now if we Bond Dev and we go to our Local Host you can sign in sign in with Google and so this is not found because our link is wrong so let's fix that um if we go to index we don't want to go to slash login we want to go to slash API slash login slash slash auth slash auth slash login slash Google let's just double check that uh here so slash API auth login Google looks good and remember we have live reload now so this if we go back look at that oh a callback is not working let's see what happens so it looks like we're getting caught here and that's because I made a silly mistake this should be not equals two perfect now again automatic reload let's go back and perfect so now we're signed in with Google and we could sign out sign in perfect now that we have our authentication working we can actually gut out even more of what's already here so let's go back and let's get rid of if we go to our index page we can get rid of this tweet creation form that's not really needed I think we can actually go to our components folder and just get rid of all of this looks good perfect um what's what's it mad about here our tweets controller we can just get rid of that as well and here it might be mad because here here and our live reload is up so if we go back looks good we can sign in sign out so awesome we have full auth working now and so now let's integrate those torso embedded replicas like I was saying earlier so before we do that let's actually see how long our auth is taking so let's go to our index and in here we're going to do what's the error these are not here anymore we're going to const now equals performance.now romance dot no and then after we do our auth we're going to console.log see if copilot can get this off time perfect now let's go back we open here we do it a couple times we can see that these are taking around you know 40 milliseconds the first one takes a little bit longer because that's to start the connection around 40 milliseconds it looks like so now we're going to do is move to an embedded replica so what we need to do to do that is go to here and you can see I already have a little bit of this setup so we're changing our config and instead of before we're just passing our database URL and our auth token now we're putting our auth token not our auth token we're putting our database URL as a sync URL and our URL as a local file so what it's going to do is create a local sqlite file that's going to sync with our database URL and so to do that we're just going to change this to local replica and let's restart our server as you can see there's the files and I also by default the database has to be synced right so your local embedded replica is all the rights go to the remote so you're only reading from the replica right because if you were trying to write to the the local one might get out of sync that'd be really hard so writes are over the network but in most apps like the majority majority of database kind of interactions are reads so reads are the most important but we have to manually sync you might think manually sync I don't want to manually sync right that's really annoying but it's actually good because you can control when you run it so in our context here I just have it running on a cron using the Alicia cron plug-in every two seconds um but we could also choose to not do that right and it's almost like a really good cache except you don't have to invalidate it you just as soon as you want it to be in sync you call sync and it talks to the primary over the network and gets all the new data so right now I just have it running on a cron and it's actually it's not blocking right it's doing this asynchronously we're just avoiding the promise it returns and then when we create our client we're calling it um right away right I would say the majority of apps don't need like instantaneous up-to-date data but you probably have parts that like do need to be uh right up to date right like if a user signs in you don't want that to land right in like the second between syncs right but because you have manual control right after an important right you can automatically sync or right before a really important read you can sync as you get full control so now let's see how fast are awesome remember they're like 40 milliseconds before so if we do this a couple times we can see we are at under a millisecond right half a millisecond so let's just confirm here we're at 40 milliseconds and now we're at half a millisecond so we're just getting insane read speeds uh with these embedded replicas and it's a really really cool feature and uh very exciting to be able to use and again this is totally free on Twitter so you can do this right now now let's focus on the multi-tenancy aspect when a new user joins we're going to ask them either to create a new organization or join an existing organization so let's start with the front end for that so we're going to create a new route called new user dot TSX something I'm going to do now is actually move this uh derived function onto our Global context object before it might be a little inefficient because there would definitely be like maybe some routes where you don't necessarily need to see if the user is signed in and that's like a database call you don't need to make but now that our database calls are like half a millisecond right it's super cheap so I'm going to take this and I'm just going to put we're going to go to derive and I'm just going to slap it on here perfect so now we can go into our routes let's actually see everywhere else we're using derived you can go here oh it's just this one place and we can get rid of this and everything should still work right yep perfect and so now let's make our new user route so we're going to export const new user equals new Alicia and we're just going to dot git slash new user okay which just turn this side and then also what we need to do is here and let's also make sure um you might notice this HTML stream function I added HTML streaming kind of like in xjs to the BET stack jsx runtime we're not going to be using it this project but again also might make another video about that so we can actually just use the HTML function here and all this is doing is setting up all the rendering for the jsx runtime so if you're using the cache function it will reset the cache the render cache and also it will provide all of the right headers and actually returns a response so it provides the headers for the browser um so now in our new user we're going to use HTML let's go here we're going to wrap this that and so now let's say we're going to actually let's make this a div and we're going to make a say each one I new user session.user.name we're gonna make sure to provide the safe attribute here so by default the jsx runtime we're using doesn't escape and that's because all j6 elements evaluate to Promises of strings so there's no way to know whether it is like a text node or a another jsx element node um the creator of Kita JS which is the jsex runtime we're using as a typescript plugin um here this typescript plugin it will actually give you warnings about places where you should be using the safe attribute which is really really helpful the reason I'm not using it right now is because it gives some false positives around like ternaries and and other kind of jsex expression stuff but it's really important just to make sure that we're escaping stuff uh and also look we're getting type errors because we forgot to use our contacts so we can use our contacts import it from Context perfect and now we're going to ask them let's have like a p do you want to join or create a organization and what's it mad about uh here because this needs to be a function perfect so now and here if we don't have a session then we're going to get our redirect function went to here and we're going to slash login looks good and then we need to get the set and the headers for that and we're going to pass it set headers that looks good and then we can also return for typescript perfect so now let's start up our Dev server again we can go here and try this out and if we go to oh and then we also what we want to do is make our callback so we go to our auth controller um not here our auth controller instead of sending a new user to here we're going to send them to slash new user looks good so we can sign out sign in we're going to get sent to new user which is not found because we didn't add it here so we're going to um import new user perfect and then we're going to add new user and if we go back looks good okay why is the Styles kind of messed up because we forgot the base HTML that's why so let's go to new user and of course we're going to want to wrap this in our base HTML component so let's do this there boom looks good cool so now let's actually we're going to call this the bath e2b SAS customer support let's go we go back look at that automatic reload very very cool so if I save here it automatically notice I'm not refreshing the page it's actually a websocket connection that the um gets set up the way that works is um you get the script which is just a little script that connects to the websocket and when the websocket sends it at an event it calls location.reload and so then in our Dev script Dev calls the live reload script which runs the BET stack CLI and that starts up a websocket server which kind of sends those messages so that's really really cool so now before we could ask users to create or join an organization we have to actually Define what an organization is so we're going to need to start with our database schema to do that so we can actually let's get rid of our our tweets or actually we can let's use this as a template so our user they're going to have an organization ID organization ID perfect and so then let's create a new table called organization and just let's make this plural organizations and these helpers are actually really really cool so by default drizzle provides us types for insert and delete or just like select or organization and if you want to um kind of expand on these to create like update and delete types you can totally do that so Alicia uses type box for validation and drizzle has a type box uh Library so you can generate the typebox schemas which we can pass to Alicia right in our handlers which is super super awesome so we're gonna let's just get rid of these um organization schema perfect so what do we want our organization to have so like ID is is pretty good so what do we want our organization to have so I think ID is pretty much a given and we're also probably going to want a name and so it's going to be a text name not null looks good now we're also going to need the database credentials right so we're going to have a database name database so let's make this like snake case database name looks good and our database database auth token right because we're creating a new torso database for every single organization so we need to store those credentials somewhere so we get access them to connect to the database later so we can let that autocomplete and then just some uh kind of standard stuff we're going to have a created at which is going to be a integer instead of being in mode number this could be mode time stamp and that actually allows us for like this type to be a date so we can provide a default function this is going to return a new date um so drizzle makes it super easy to work with dates because it handles all the serialization deserialization and so that looks pretty much good we actually really don't need any indexes here we're only ever going to be looking these up by ID most likely which is already an index we can get rid of this and now we can rename this file to organization and that's actually before we do that let's um let's push all the other changes we've made let's get rid of this this here all those other changes we're going to call these Google off let's commit looks good so now uh in our index we're actually going to get rid of these relations here and all of these we're going to export organization from organizations so now back in our user table this organization ID this should be an ID ID is going to reference the organizations in the organization table so we're going to create that in code with Drizzle sorry to export our user relation it's going to equal a relations from some new super relations drizzle and this is going to take the user table there's going to be a one to many right because one uh a user only has one organization but an organization can have many users so this is going to be a one here and we're going to return an object which has an organization organization let's be one and one is a function that takes the table uh here and then we can pass in an object so the field on the user table is going to be the uh user Dot organization ID and it's going to reference organizations.id and there we should be good I want to go in here and we want to export that relation user relations so let's go to our organization so let's create that menu relationship we're going to export const organization relations it's going to relation it's going to equally relations can be on organizations it's going to be a many return and object it's going to be actually don't need any of this just one like that perfect and so um the way now you know some of these like built-in Lucia tables and these are just copy pasted from the Lucia docs they have these um dot references and these are setting up like actual foreign key constraints but these relationships we're setting up don't have constraints they're just kind of defined in the way we're querying the data and the way we're inserting the data um and so what this allows us to do is if we go to our DB here we can db.query dot organizations dot find many and we can with uh users true if we look at the result cons to result you can see that we get users never and the reason for that is because we never exported the relations now that we do that back here look at that full typed fully typed pretty pretty awesome so that's cool right but um now we need to set up the schema for that organization DB um this is a little bit hard to conceptualize right like doing uh schema management and migrations for one database is hard how do we do that for like 10 or a thousand um it's actually going to look pretty similar to how we do the normal database and so what I'm going to do is I'm going to create a new folder in the DB directory called primary I'm going to take our schema folder and our index I'm just going to drag it in there I'm going to create a new folder in TV called tenant and this is going to have basically its own one of these so I'm just going to copy these for now and put them let's make sure we have the right stuff selected copy copy paste actually copy too much copy too much here copy and paste perfect let's see what it's mad about just some import stuff so let's fix that are these all good these look all good um and also let's let's take the time now to let's just uh update our drizzle config because we moved it so this is going to be slash DB slash primary and then other than that we should be able to bond DB push with no problems here and we're creating a new table yes I want to remove one table perfect okay and so we created that organizations table and we added the organization ID column to our user table perfect so now what we're going to do is set up the schema for our tenant databases and also the way we're going to create the client is a little bit different now we don't actually need all this stuff because it's always going to be a remote client now it would be kind of cool to set up like a local replica for all the tenants um but it doesn't make a ton of sense mainly because the tenants are kind of like our primary is always being read but the tenants are kind of being read on demand and so it makes the most sense just to connect to them remotely when we need to so we can get rid of all of this and we actually can get rid of this as well and so what we're going to do is just instead of uh we're going to do have a function we're gonna have a function coming export function it's going to be called git tenant DB and this is going to take a DB name and a auth token these are both going to be strings here we go um and so what we're going to do now is our cons DB whole URL is going to be a template literal here and so if you'll notice from our environment that all the uh terso URLs have this like same pattern right so we can go back to here um and so it's the name of the database DB name um and then this is your turso username or organization name looks like I emitted a little error there so we can set this up and then we're going to create a client with that so now we have our client for our tenant database and so now we're actually just going to make a drizzle from the schema and the schema is our tenant schema right this isn't this is separated like right now it's identical but we're about to change it so we have our primary schema and then we have our tenant schema and so we can get rid of this get rid of this and get rid of this and so we're going to call this const tenant tenant DB equals drizzle now let's call this tenant client and so what we're going to do is we're going to return our tenant DB and our tenant client I'm going to call these if we're going to keep these the same because um in our handlers we're also going to have access to our normal DB so we don't want to name that also DB ink get confused right these are these are different things um so now okay great we can connect to the DB uh but how do we push right how do we push um and so now we're going to have a function called push to tenant TV we have a very similar signature here so we can just copy that and so what we're going to do in here is actually we're going to use bun to spawn a new process with Drizzle kit uh to run all the migrations for us and so the first time we do this when we create a database you know like you can see that when we there are like some like manual uh things it asks us for and the way the reason we don't need to worry about that is because the first time we create a database when it has like no schema at all it won't have anything to ask us and then if we're doing migrations then we can do that manually and I'll show you how to do that so we don't need to worry about it in our code because in our code we're only ever creating the database and pushing to a completely fresh database and then migrations will have to handle a little bit more manually but I'll show you how to do that so the main thing here we're going to do is we're going to const procedure equals we're going to bun.spawn and here we're going to put in an array we're going to bun X drizzle kit push sqlite um now if we go to the drizzle docks you'll see that um they have a way to use the CLI to do all this which would be really nice for us now most people do it with a custom config file which makes sense but we're actually it'd be super helpful for us to do with the CLI because we're running this like in application code right not from the terminal however um there's a little bit of an issue so the first thing is that um if you notice in my drizzle config there's this tables filter basically torso has some and lib squeal which is like the kind of sqlite force that turso uses has some built-in tables which are like shouldn't be accessible to the user like drizzle knows about these and so they filter them out but there's one in particular which is the wasm function table which is actually kind of cool you can create custom wasm functions to like run in your sqlite queries which is awesome but uh drizzle doesn't know about it and they're working on fixing it but for now we just add these table filters here the problem is uh if we can actually see this if we go to bun x uh drizzle result kit help push SQL light what you'll see is like schema has like a schema input tables filter should have an input here but it doesn't and so it doesn't work um I've already talked to the drizzle team they know about this it's definitely going to be fixed in the next drizzle kit release but for now um we can't use the CLI options but what we can do is write a temporary config file that we just delete after we use it um I guess a little bit slower but really not a big deal so let's set that up so what we can do is take our existing drizzle config file and let's create a new in our tenant DB we're going to create a cons config text it's going to equal this um and so we want to do here is we want to get rid of this rid of this this is fine this is going to be tenant that's pretty important right we want to take our tenant schema 10 in for our database credentials uh this is going to be an object with our perfect that looks good actually we're missing this part here so now this is should be everything we need and what we're going to do is we're going to create our just make this a variable we're going to have a const temp config path and it's going to be in Source DB tenant um slash config.ts let's call this drizzle I'd config.ts and then we're going to add in here is we're going to uh await Bond dot right and we're going to write in the temp config path config text this is going to be an async function perfect now we're going to add this here as a dash dash config perfect so what this is going to do is it's going to spawn a new process um that is going to push r schema our tenant schema to whatever name and auth token we pass to it so Bond spawn does this asynchronously so we want to make sure that our application is able to kind of account for that so we're going to create we're going to return a new promise that has solved reject and in here Let's uh we're going to pass in in our second options we have a on exit and basically if the xcode is 0 we're going to resolve let's just uh void zero here otherwise we're going to reject and let's just for good measure if here let's console dot error um there just kind of let us know now the final thing we have to do is make sure we clean up our temporary file here so bun recommends using unlink sync on the link sync from node FS and we're just going to unlink that perfect now the final thing we need to do is I was talking about migrations so this will just run um and it doesn't take input it doesn't really output anywhere but let's say we might want to uh kind of manually do the inputs so we're going to have a new prop or an prop new argument we're going to call this input and it is going to be a optional and it's going to be an optional Boolean now if input is true we're going to want to do is set standard out centered out and it's going to be based on input so we're going to set it to inherent or undefined so if it's inherent inherit what that means is it's going to inherit the running process right so this is going to be false in our app code right and so this isn't going to have any effect we're going to do the same thing for standard in but I'm going to set up a migration script in a second as you'll see where when input is on it's going to basically feed the terminal input and output that we see uh like through to this running process which is going to be pretty useful finally we're going to have a new thing called migrate migrate dot yes and this is going to need two things it's going to import our main app database it's also going to import this push to tenant DB and so we're going to do is we're going to get const organizations equals db.query Dot organizations dot find many right and then we're going to organizations.4 each or oh we have to always see that for each and this is going to be an async function it's going to take the org what we're going to do is await and also we want to make sure we have input to true in here and this is going to be database name and database auth token we're going to finally console.log pushed to tenant DB perfect so now let's set up a little script so we're going to call this DB push tenants is going to Bond run dot slash Source DB tenants or tenant I think I should migrate.ts so this is pretty good but what are the actual schema we're pushing like what does each organization need in its own database and so what what the organization databases are going to hold is it's going to hold a it's going to have a table of all the tickets and a table of all the chats and a chat is going to have a reference to a ticket so a ticket's gonna have any chats a chat has one ticket and that's actually all the data we need to store for an organization and it's actually good to keep this kind of small like only the stuff that's absolutely critical to that organization we can keep there and again that way when the organization wants all their data we can just easily sqlite dump all their tickets all their chats all to a single file we can send to them so let's set that up now so I'm going to let's just get rid of this and let's get rid of this and here we can just get rid of these for now so let's create two new files we're going to have tickets.ts and cats.ts so a ticket Let's uh import here so const tickets equal sqlite table tickets and what does a ticket need so first it needs an ID which is going to be a thank you so you'll see that copilot is recommending an organization ID here which would make sense if we were in one database but remember we actually don't need a reference to what organization this ticket is in because it's in its own database so we always know what organization a ticket belongs to because we know the database we're reading it from right what is kind of cool though is we're going to give this a assigned employee ID we're actually not going to take advantage of this in this tutorial um definitely something maybe to add later but uh it's something cool just because you can think about this is referencing the users table in another database um right so we have all of our users in our primary database right because that's auth right like we want to make sure have like full control of that but these tickets have the employee ticket ID which references another database pretty cool now let's give this a subject and a description and a status and we're actually going to use the drizzle enum this is going to be open or closed perfect now we're going to have a created at created at this is going to be a integer created app mode and this is going to be time stamp timestamp uh no we're gonna have a updated at this is going to be null because it's not updated at the start and finally we're gonna have a closed app so this looks pretty good for our table so we are going to Define one index so we're going to define a status index and that's going to be a index on status index on table dot status on tablet status perfect looks good and then we're going to Define our chats just copy this chats chats and before I forget let's go back to tickets let's export uh the type export type ticket equals tickets Dot and first select and we just need type of here okay so now for our chats table um we can get rid of some of this so we can keep these we're just going to have one timestamp um and that's when the message was sent and then here we're going to have a sender and this is either going to be the employee or the customer and the reason for this is because we're not authenticating customers we just want them to be able to visit the site and type messages so there's going to be some kind of uh it's pretty loose right like just either an employee sent it or a customer sent it then we're going to have a message field and then finally we're going to have a be a ticket ID and this is going to be a integer integer not an old and this is now we can set up the relation so let's export chat const const chats relations this could be equal a relations chat many and let's see there we go I don't know why I commented that out table dot status oh indexes of course so let's define an index on the ticket ID of course because we're going to be looking up the chats based on the ticket ID just all the time and then we'll also do this by timestamp so we can look at messages like before or after a certain date so let's timestamp index perfect then we can import our tickets uh so it's not exported uh my bad my bad we're gonna export that we can also export that we can import tickets here perfect and then we can go back to tickets and let's define the one two many relationship wait I think I actually got this wrong yeah this should be the the one to one or the one to me this should be one so uh ticket maybe one tickets then we're going to the field is going to be the uh chats.ticket ID it's going to reference tickets dot ID perfect now we go back to tickets and Export cons tickets relations relation tickets and this is going to be the many you can go here and we can chats any chats boom boom what am I gonna say one more one more then we just need to import these import and import and then we're going to export so let's do chats our relations for such chats and now if we go here now again R we're still importing this schema here and so the DB we return as client DB we can tenant DB and in Dot why is it doing this NTB Dot and we can query Dot tickets I find many and then we can with boom chats true and if we look at the result we get oh come on here we go pretty pretty cool awesome so let's push all those changes set up tenant databases commit looks good I see a red what's the red um let's try restarting the typescript server okay here this needs to be slash a dot dot slash DB slash primary okay here uh this needs to go I think one level up um let's see right here okay organization see where are we ah I don't know how this got there import user from uh let's make sure these are all oh we're good there we go fix itself nice let's get rid of this oops commit perfect so I know that was a lot of work with uh nothing really to see kind of as the fruits of our labor but it's all going to come together really quick so let's see so it's definitely been a minute since we touched the front end so let's go back to that if we go back to our new user page that we were working on so so long ago uh we can add a form I'm just going to leave all this stuff unstyled for now we'll worry about the styling later we're going to add a input uh let's call this name organization uh it's gonna be type organization name cool let's have a submit button button create organization now let's give this a type equals okay so now let's uh is our server running I'm gonna run our server and oh let's see here forgot to update our import this needs to be primary primary slash b or DB slash primary boom looks good so we can go to our now we can go back to our app and here we have our form let's actually just make this a little prettier for now it's uh class let's let's be semantic let's make this a main class equals Flex Flex call um with full how does that look items Center just defy Center okay and let's give this class text 3XL font bold [Music] okay it's pretty ugly let's give this like a gap Gap five okay and then let's do something similar for here uh I'll just copy these we don't need the width to be full I do not know why these are like offset that's really horrible whatever we're about the Styles later okay so let's just start with creating a new organization so we need something on the back end for us to send this to so let's create that endpoint and also this is uh kind of annoying so I'm just going to stop that for now we could actually uh if you want to turn off the logging for that uh you can it's going to be in context and that's just uh this log right here and this log right here the reason I have them turned on is because uh I think I was just curious about how long the database syncs were taking um some of them take a little bit longer than others it actually depends on like how much data um is out of sync and also like the first one uh this could be a little bit longer because it has to set up the connection um but you should just be able to run this in the background no problem just make sure not to await this right because you don't want to block your entire server uh every two seconds right so just make sure to just avoid this promise um so let's go to the back end we're going to create a new controller called organizations or organizations.tsx and we're going to Let's export const organizations controller I knew Alicia I'm going to use the context and that's fine for now we got to import this and let's go here and let's dot use organizations controller and then we're just going to import that looks good organizations let's actually let's not make this boils for this organization you can go here did I spell it wrong is that why okay there we go so let's create a new post endpoint um actually this is going to have the the prefix prefix perfect now let's create a new post for Slash and actually let's we don't really know what we need for now so let's okay let's just leave the Handler blank um and then what we can do is we're going to have the body and that's going to be a this is the validation by the way this kind of so we have our Handler we have the route the Handler and then here we can add like um validation or hooks so you can add like before handle after handle then we can also add um the schema parameters and so these are going to be type box schemas with the T variable from Alicia and like I said the schemas that are generated for you by type box where is it um the like drizzle type box you can use these just right in your Alicia um validators here um so body is going to be a t dot object and we're going to add organization name so let's make this name and then we're going to want to update this on the front end so let's go back to a new user to add name here perfect and so what this is doing is when the request comes in it's going to make sure that the body is in the shape of this and what that means is if we go into our contacts and we get out our body we know for sure the type of this thing because it's inferred from our schema which is being validated super super helpful so now let's set up the logic for this controller um the first thing we're also going to want to grab from our context is our session right we want to make sure user is signed in so if there is not a session we're going to redirect them this is going to take the set and the headers so we're going to redirect them to slash log in um looks good and so then we're going to return for typescripts return perfect so we could create the organization right now but I think the part that's like most likely to fail is the database creation so let's make sure that database creation Works before we actually create the organization Row in our database so to create the ID for our database we're going to add cuid so to do that we're going to bun add cuid2 and let's go to our lib and we're going to just pump this down we're going to import uh init um from at parallel drives your id2 we're going to cons create ID equals init length we're gonna do seven okay we're going to export function create database ID and we're going to return create ID perfect so we can go here let's const DB um name I think DB name and that's going to be we're going to do org Dash create database ID perfect so we're going to have org and then a hyphen and then the length 7 Cyd so now we're going to do is we're going to get um the we're gonna go into our contacts and we're going to import from Beth stack Beth stack we're going to import the Torso client from Beth stack slash ter so and this is just a little wrapper I made around the turso API so you provide an API token and it basically does all the the fetch calls for you and types it um so we're going to create const turso new torso client and it's going to ask for our turso API key so let's go into our config and let's add that so we're going to add a torso API key let's get a string Min one and then we're gonna go to our EnV we're going to add terso API key equals here and we can just throw this in the example as well let's also add the hostname over here host URL so let's go the the turso CLI and I think it's torso auth was it terso off API tokens create API tokens mint that mint was it oh he needs a name to perso Mint and let's call this YouTube or subscribe it's called subscribe subscribe there we go and we can get that token and put it here perfect so if we go back here why is this mad because we didn't give it our Dot N dot turso API key perfect and now we can just throw this on our context now when we go back to our organization here we can just pull out that tereso client or so and there it is nice so now let's const result equals a weight we're going to turn so wait tereso dot database databases dot create um what is this one you want to say name name and a group so uh this is the one thing that might be different by the time you see this video so in with tereso there's this idea of database groups now if you're on the free plan you actually don't need to worry about this at all you just have one default group that can create up to 500 databases um so this might change you might actually not be able to uh put uh you might not need to put a group here at all so don't worry about that and I'll leave either like a pin comment or something in the GitHub repo about the the way to to do that um right now um but right now I need to make a group so we're going to terso group um and the reason that the groups are kind of useful is because they allow you to keep databases uh separated right so replicating um you can now replicate groups so this is important where if you have like some data that needs to be kept in one region in some data that needs to be kept only in another region maybe for compliance reasons you can put them in separate groups and replicate them to separate regions uh completely isolated which is super useful so turso we're going to create a terso group pre and we're going to call this tenants and any moment now perfect so we can put this as tenants and what this is going to do is it should return a logical database result for a database with this name in our attendance group and again if you're on the free plan you shouldn't need to worry about this but definitely check the GitHub repo and comment for what that API looks like at the time of watching um and then from this database what we're going to get is the only thing we really care about is the name so now that we have the name the next thing we need is the token so we're going to const JWT equals turso dot I think it's logical databases we want to weight this as well logical databases Dot mint auth token and what does this want it wants a org slug DB name so our org slug you'll know this mine is just my name and the DB name perfect so that should be our auth token so now that we've created our database we want to push our schema so we're going to await push to tenant DB and we're going to give it our here and what is this one what's the DB name auth token come on auth tokenjot perfect so now uh we made the database then we made the auth token for the database and now we're creating a drizzle kit process to connect to that database and push our schema and again uh this should almost always go off without errors because this database should be completely empty so whatever the schema is it should just always push now once that's done we're good to go ahead and um Constable just results equals a weight we're going to pull out our database from here DB we're going to DB Dot insert into organizations organizations dot values we're going to dot returning as well turning and so the values it's asking for are the name which we have right that's our body.name our database name is the name and our we don't need a user ID we need the auth token database auth token GWT perfect but it looks like Oops I messed this up maybe don't use autocomplete okay this should all be happy now we're actually going to return ID and it's going to be organization side ID so this result um should be yep and so we actually should just get one back so we can destructure that and if not result we're not going to throw a new error we're going to set Dot status equals in internal server error and then we're going to return like something went wrong okay now the reason we need to get this ID back is because we need to update remember our user we go here our user table as an organization ID and so when we create an ID we need to assign the user that created that ID that organization we just created so now what we're going to do we're going to uh await away DB dot update user dot set we're going to set there we go that should be good I'm going to import this and it looks like we made a little bit of an error when we made our our user table this should be an integer because the uh the type of the organization table ID is an integer right so now that we have that fixed now we can bun TV push um let's see what it's doing should be good oh and it's getting mad um looks like our database got a little messed up but again we're prototyping here so it's all good so we can just uh shell into Beth and let's let's look at let's look at the tables um so we're just gonna drop table um let's just let's drop the whole thing right we have basically nothing in here not a big deal we can get rid of this user key user okay perfect so now we DB push looks good okay so now again if you like have actual data make sure you're not just deleting data but now just for creating a little simple project schema changes are hard to get right in the first time so it's okay to just kind of move fast there so now when we create the organization we're going to set that user that created it and remember we know the user because of the session right um and so uh yeah we actually need to add aware that's important we don't want to set the entire every single user since we're going to wear and we're going to import equals from one equal from drizzle user ID session.user.id perfect um so now especially since we're using uh the local replica we're going to add a new um function to our lib called export function sync if local and this is going to be a async function we're going to import our client await and wait client dot sync uh but we're going to wrap this we're going to if config Dot end dot node and uh or actually not our node ends we care about our database connection type if this is equal to local replica then we're going to do this otherwise we're just going to return right and the reason this is important is because and the reason this is important is because like after um this is like I would consider an important update right and because the rights again they're going to the remote replica so we want to um immediately get that data on our local replica so we're going to await uh sync if local and now that we after we kind of assign them an organization we're going to redirect them to the the dashboard which doesn't exist yet but we're going to make soon and then the final thing here is we're going to add a Min length in length of one and a fax length of I don't know 30. um to our organization name and again very convenient to scheme of validation from Alicia so now this is if we want to create a new organization right what if we want to join an organization so we're going to actually do we can probably like copy paste most of this and this is going to be um slash join right and so we don't need to create a database right because it's already created and we don't need to create an organization because it's already created what we need to do is get the organization that the user is joining so um the code we're going to use to have like existing users give to new users it's just we're just going to use the database ID so here we're going to have the organization name and we're going to have the database or like the organization this could actually just be this should just be the organization code right this should have a length in length of it's the org plus The Hyphen plus the seventh lengths uid so it should be that's 11. so we're going to say Min length is 11. X length is also 11. perfect so now what we're going to do is we're going to get the uh the organization code here um that's actually really ugly we're going to destruction that not there so we're going to const result equals a weight and we can use db.query Dot organizations dot find First and this is going to be where we're going to pass in our organizations table and we're going to get the equal and dot database perfect so basically what we're doing is we're getting the first organization where the code that they pass is the database name and we actually don't need to destructure this this is just the organization because we're using find first so it knows it's only one and then if not organization we're going to say say not found not found and we're going to return organization a couple blocks nice okay so now we can set the just organization.id perfect and looks good cool right so now we have um two endpoints right slash organization slash organization and slash organization slash join and so now we can go back to our front end and let's make a second form and this one is going to be let's say um what did we call this your organization code it's going to be join and so is our server running let's run our server that's not our server you can bun Dev C okay and we can go back here sign in with Google not found because uh basically uh this was wrong such API off let's log in Google and there if we go back with Google uh so I just spent a couple minutes debugging basically what happened was um when I introduced this sync if local function uh there's a couple more places we need to add it besides the place we did um so where we have it right now is after we uh create an organization or assign a user to an organization but there's definitely some places in the authentication flow where we'll want to make sure our database is in sync and basically one of those places is in our um oauth flow so after we get a new user from Google I'm assuming everything goes well we like make their session uh before we send back that session cookie we're going to want to wait sync if local um and that's just so when you're logging in for the first time uh it does not immediately redirect you to a page where the database is slightly out of sync so now if we go back we sign in sign out should be all good perfect so where were we new user I'm accidentally this not here here here here boom new user right so sign out sign in get redirected to new user and we're at this form which I know kind of ugly again Sally can come in later um so here let's go back to that new user page now let's pull up our endpoints as well so we have our slash endpoint or slash join and so what we're going to want to do is let's start using that htmax right so let's have our classes first so here this is one's going to be each x uh post it's going to be slash API should get some type safety right pretty cool API such organizations and this is going to um because like this name right it's already it's automatically going to be put in the form data um so that's should match here and then for this one we're going to look at that um so now let's again uh what's wrong cannot find form that's not good uh and then just to kind of work this out let's create a new file called dashboard um or dot TSX and we're going to remember because we're redirecting there after we uh assign or create a user right so if we create an organization or we assign an organization we're redirecting them to dashboards the dashboard is going to be um a new route to Alicia and that's going to be word const let's import this number by two dot get slash dashboard and we're also going to want to use our contacts right and so now we're going to um we're going to need our DB we're gonna need our session and let's just set that up okay and so here we're going to make sure that someone here is belonged to an organization so we're going to const uh organization equals DB dot query dot organization dot find this could be organizations sci-fi first and that looks pretty good um and session right if there's not a session and we're going to redirect and we're going to need our set and our headers set headers to redirect it's going to take uh login look at that it knows what to do return so that should be good and uh the reason that our user object does not have this organization ID is because if we go to our table here right we have this organization ID but it is not in our Lucia types right so we need to let me pull that up again not here here um so we need to go to our Lucia types and add organization ID and then also we need to go into here where we get our user attributes in auth right so our data now we can organization ID equals a.organization.id and this should go away so first we check if there's not a session at all and then we want to redirect them to slash login but if there is a session and there's not a session.organization ID then there are new user right so we're going to redirect them to new user and this should go away um what's wrong uh uh I think this is because const org ID equals there we go I don't know why typescript does that that's a little weird but um that works so now we have our organization and we need to await this actually uh and if there's not an organization um yeah this shouldn't be possible right or it could be possible like let's say uh this organ because remember we don't have uh foreign key constraints right so this organization IDE could be not an actual organization right so what we're going to do is uh like that this could return nothing so we need to direct them to new user if so now what do we want to return here uh return so let's return some HTML right so we're going to have uh base HTML um and just for now let's have a H1 with our creation.name okay looks looking looking good okay let's import this oh it's already imported perfect okay so now if you go back here and hopefully what's going to happen is when we um we're author use new user page right so when we uh submit this form it's going to send a post request to our endpoint which on the back end which is here to send this post request right we're going to get a session we're going to get the body we're going to create a new organization and then wait this is join up here we're going to make a DB name or create a new database create a new token for that database push drizzle kit push to that database then we're going to get that organization set the um or create a new organization with these new values and set our user to that organization and then redirect us to dashboard right so all right let's see what are we going to call our organization we're going to call it ah man what do we call it like the best Beth squat Beth squat okay what went wrong oh let's see let's break this down oh oh it did go through it went through we got redirected okay there's a lot going on here um in our base HTML I think we have HX boost turned on right so what HX boost does is it takes all the links on the page and instead of like fully redirecting you to that page htmx is fetching the content in the background and swapping it in and so basically when we get uh redirected because of our like HX location redirect thing um it's fetching that page and what happened was I never add a dashboard uh to here so we need to dot use dashboard Dash board do we never export it we did import here we go so now if we go here that's not good uh because I forgot to add the HTML function um but that was actually interesting right so the success went off successfully hopefully right so we'll see in a second um okay so we know the organization has been created so let's actually let's DB shell uh terso turso DB shell Beth right and we're going to select uh star from organizations and so okay looks like the data it actually got created um twice I think I submitted the form twice and it because it failed the first time so if we ter so DB lists that's all right terso DV list okay we'll see those got created um so I'm actually just going to turn so DB destroy this first the second one because um yeah we don't need it yes and then I'm also going to let's go back into our shell go back here and we're going to delete from organizations where ID equals to okay then we can just make sure okay so cool so we know it went off successfully we can see again if we tursodb list that database got created and now here's the real test right let's copy this let's turn so DB shell in here and what we're hoping for is when we view the tables they should be there and they are right so that's awesome um so our migration script here right uh not here here when we respond a drizzle kit process and it went off and it made these changes to the database which is super super cool awesome so this is a really good spot let's uh let's commit here we're going to say added create organization that was let's commit so before we move on this form is really ugly and uh let's make it kind of nice um so we're going to take advantage of a couple really cool hdmax things so the first is we know that these uh apis right they're redirecting us if they're successful so what we can do is swap the rest if if we get an actual response it's not a redirect we know that it's probably an error so we can set up a little uh we can set up like a div right it's gonna be class it's gonna be text uh red 400 maybe we text large um we're just like normal size text and it's just gonna be just to be chilling here um and what we can do is in our form right 0.2 HX swap equals and this needs an IDE so we can Target it six equals uh create error message right we can just call this error message so we're just going to call this error message um and then we're going to HX swap equals next #error message and we're going to or this is HX Target my bad HX Target and HX equals inner HTML perfect so now if this request right uh let's open that up that's here here so if this request uh fails um like here we're going to get this internal server error and it's going to be swapped in you would think but htmx is actually kind of smart right like by default it doesn't swap in uh non-200 responses or like non-okay responses and so what we need to do is use a hdmax extension so let's pull that up so we're gonna go to the htmx documentation and if we go to the extensions we're going to use the response targets so what this allows us to do is set like an HX Target like 404 HX Target 4xx and so to do that we're just going to add this script tag to our base HTML oh I already have it added awesome okay so what we're going to do is we're going to in here uh in this uh div we're going to HX extension equals response targets and then here we're going to HX Target like 4xx and oops and 5xx um and I've actually I was looking at there's a PR for htmx where they're looking at adding not even as an extension like just to the built-in vanilla Library you know like HX Target error or HX swap error because this extension is cool but it's a little bit too granular right like most people don't care about the specific specific status codes like it's really powerful that you can but most people probably don't um and so separating it into just like a binary like success or error is super helpful and then also boss targets extension uh doesn't allow you to change the swap Behavior so you're looking to add that so you know just kind of keep a look out for that um maybe coming to hdmax very soon um the next thing we're going to add is uh loading States right loading States um can be kind of difficult but htmx makes it super easy so by default they have a um if we go to the website here uh they have classes okay so there's this class called hmx indicator which you can use to like provide add special styles to a certain element during an htmx request but there's actually another extension we're going to use and it's going to be called loading States so let's get this script tag and we're going to throw it on our base HTML and we're going to throw this thing on our whole document loading States look at that what this does is it gives us a couple attributes to add to elements that we want to only be shown when an htmx request is loaded so let's see what that looks like so we can um let's just make a new form for example and we're just going to have this like slash test so let's just go to our organization add a dot post slash tests and this is just going to sync there we go okay so it's just going to take one second so we're going to slash test and then uh have a weight so then we're going to add the data loading States attribute to the parent element here this is just going to kind of mean that we don't have to like give a bunch of unique names everywhere what we're going to do is we're going to add a div and we're going to give this a class of this is where we're going to use the Uno CSS icon so we're going to add I um we're going to use from the lucide package I lose side spinner animate spin there we go and so then if we uh go back to here why do we not see it let's see let's go to am I typing it in right spinner Ah that's why loading here so let's get this loader to then we can so now if we go back here there so we can see it's loading and so that's just like one class and we have we have all these icons and they have a million icon packs super super cool and this is actually you can control the size with text so we can text XL XL and it will be larger hopefully 3XL Maybe there we go so cool uh we have a spinner right but um right now it's showing so let's add the data loading attribute to this and then there's one final thing we need to do which is add a custom uh Styles class to this data loading attribute so we're going to style and we're going to add here and we're going to add data loading there we go perfect so now if we go to the browser we won't see it but it appears and then it gets removed because um this is like swapping in itself with the response but pretty cool we have full loading States now the other thing we can add is and let's actually just um HX swap equals none right so now if we go here okay pretty cool so now what we can do is uh let's just add some classes to this button so we can um uh PX4 py2 border border black BG Gray 400 hover BG Gray 600 um why isn't the Uno CSS extension working all right so there we go just had to restart vs code but um now we can see the colors and so let's also add a set a disabled um BG red uh there we go oh I forgot to spell right there we go okay nice so um not very useful right now because nothing's disabling it right well ready for this data loading disable oh because I never look at that okay so now we go back look at that super cool okay very minimal code tons of behavior awesome all right so now let's really pretty up this form right let's let's get those Styles in order so um the first thing we're gonna do is uh kind of our overall layout uh so before we were using um h-screen um on our body element but I'm actually going to remove that just uh for some additional flexibility we're also going to remove um this kind of banner here and so now we're going to put H screen in here because we want this to be like that and let's make this BG gray I don't know 200. so that looks okay looking pretty good honestly I like that now let's clean up this kind of text at the top so let's change this to like thanks for signing up user please create or join an organization to get started we can get rid of this let's add some extra things here so let's make this um let's just semi bold and let's text Center see how that looks that looks fine we need to do this okay so now let's make these forms look good uh I'm gonna be completely honest design is not my specialty when v0 comes out I'm going to be abusing that to hell and back so I did uh spend a lot of time kind of preparing some styles for this tutorial um but I'm not going to spend the time kind of showing me writing out each class so I'm just going to copy paste the ones I have and I'll talk through them but um yeah this is not a design tutorial hopefully that's okay with you so first on our form we're going to actually remove Flex because we don't need it because what we're going to do is add a block to our input in a sec class equals block um and here already that looks pretty good um the input and the button needs some styling there were also um we're going to add a label for organization name and we'll see how that looks um now on our input uh so I've gone ahead and added some Styles you could look at these in the um in the GitHub repo in the description if you're curious um but looks a little bit better you know got some color now got some some hover effects another thing that's actually really really cool is you know if you were doing a forum in JavaScript you would be doing um there's a lot of like in JavaScript validation but because we're using the platform we can just use the platform and htmx kind of works with the HTML validation API where it will automatically make sure that all of your inputs are validated before it submits a four so what we can do is on our input we can put required equals true and we can also set like a regex pattern and so what we can do is add a regex right just to um try to make sure it's like a through z either uppercase or lowercase or one through nine and then if we go here and we try to like weird name it's going to tell us right like you can't do that um so super super useful and we could actually we could even um like go in here and what do we have like a Min length One max length 30 so we can go in here and we can Min length equals one max length equals 30 and so then if we try to submit this oh try to submit this again um it's not going to let us I don't know why it's not showing that but you can see it under there so just really cool example of you know validation loading States um and you can see also we we added this um the spinner here this data loading spinner so if we were to actually or I'll I'll show that in a second when I uh so I'm going to convert the second one and show you guys what that looks like all right so I've prettied up this other form here and uh looks like I forgot one thing let's uh what do I have a SpaceX three or space for I3 space Y3 um so to show you guys what the error handling looks like right so on either a 400 or a 500 we target the error message join which is all the way down here right that's the red text and so if we like uh just early return an error then we go to our form and we do this we can see we get that error back on the front end and that's super cool right and so now that we have this set up let's actually test what it looks like to join instead of creating right and so we know our our joining is um with the database code so it's actually like torso DB list so we can get this code if we run our server again I'm going to go to a second account here um okay and we can join with the code oh and we forgot to just switch this back let's try this again and hopefully we should join the organization and then be redirected to the dashboard so let's try that out this is the wrong one here we go and there we go so now what we want to do is the thing is if I still go to this page even though I have an organization uh it looks like I don't so what we're going to do is we're going to add at the top here and we're going to get our DB DB and we're going to const organization it's weight DB dot query dot organization that's all right organizations Now find First and we're going to where and it's going to take organizations and equal and here so now we're going to check if the organization.id equals the session.user DOT organization ID okay there we go so yeah there's this very weird stuff I don't know why extracting this out to a variable um makes typescript happy but it does so anyways first uh we check if there's an organization ID and then if so we need to check if that's an actually valid organization and then we're going to if organization uh we're not going to do that we're going to redirect to slash dashboard slash dashboard cool and let's copy that here well so now if we go here we should get redirected to slash dashboard all right found a bit of a mistake there so this should be if organization not if not organization and now if we reload now we also have to return right here that's why if we go here and we get redirected to our dashboard awesome cool so that's our new user page complete and now we're ready to get started on our dashboard so let's uh first push we're gonna say new user page complete and commit so now that we're kind of sending all of our paths lead to dashboard let's make that dashboard um the one thing we're going to do before that though is if we are signed in we're going to add one more option with side site out we're going to add visit dashboard not that dashboard there we go this is just going to take us to slash dashboard and let's make this like green I don't know and then let's uh fix the hover styles green and green okay so now get sign out sign in and we get directed to the dashboard but if we go to here look at that perfect okay so now let's work on that dashboard page so um yeah I kind of mentioned uh styles are not my thing I'm going to copy paste in here just a kind of layout to get us started and I'm going to talk through what that is but yeah just expect a big blob of HTML to appear all right so this uh didn't end up actually being that much um so here's what we just have is again our base HTML basically just leaves us um an unscripted body so what we're going to fill that with is a div that's going to take up the uh full height of the screen it's going to take up the full width it's gonna have Flex column and the reason for that is we're going to have this kind of sidebar and this main section um and the main section is height full so it fills up this whole thing in the sidebar is also height full and it's going to uh have a A Min width and a specified width here in large and then um it does like something like I don't know I'm not really uh focused on getting this super mobile friendly right now right so um and we just got uh some a header and some some content here um so now we're going to add some tabs uh to this sidebar and so this dashboard item is just has an anchor tag and a div with some logo and a text right so um so our first dashboard item is just going to be um home it's gonna have the lucide icon from home and just going to take us back to slash dashboard so if we go back we can see there it is then our second icon or kind of like item here is going to be sign and it's going to have the log out logo log out that's going to take us to slash log out my bad we actually have a API route for this right um and so if we go back here we should be able to sign out now our next thing is again this is a customer service app right so we need to have a tickets page so we can go back here it's gonna be tickets and now that link doesn't exist right now but we will grade it in due time just to kind of help uh fill this space we're gonna add like a settings and I need help um and this just kind of helps make it look a bit more fully even though we're not gonna to implement those necessarily and then finally we're going to add a item here it's going to be called I'm not admin we're going to call it organization uh it's quotes there and let's do this slash organization okay and then if we go back here um let's make sure we got that icon right so let's go to lucide let's find like people person company uh uh a group what's a it's a logo for a company business brief case not a briefcase ah here I like this user Circle um icon so we're going to go with that and if we go back here there we go great so now we're going to have different links for all these pages and we want the dashboard to kind of be like a consistent layout between them so we're actually going to extract this dashboard into its own component right so let's create in our components folder we're going to have a new file called dashboard.tsx and let's just copy this all of this um went to export function dashboard um then we need to move this here okay should be good and then also this is going to take a children and the type of children is going to be of we're going to get the props with children props with children type from Beth stacks jsx and then we need to in here um put the children okay good and now we should be able to replace all of this with the dashboard component let's just say hi go back here perfect looking good so now when we create these other routes like this organization and the tickets um we can share this layout between them so on this main dashboard page we're going to just kind of have some analytics this is nothing super deep but just you know something helpful to the user so we want to show is like the number of tickets open the number of tickets closed like um how many tickets were open this week and so we're gonna need to get all that information from the database first now remember all the information about the tickets is in the tenant database so the first thing we're going to need to do is tenant DB equals get tenant DB and we're going to need to pass in our what does it want name there we go so we have our tenant DB oh that's why I keep uh let's make sure I don't screw up any other thing so we can DB DB here we go so first let's get the number of all the tickets that are open so we're going to const result equals tenant DB dot select Dot from we're going to import our tickets table and then dot where then in our where Clause we want to check that they are tickets Dot status is open okay looks good now uh we need to import this to import it so I can make types go happy there we go okay and then right so this is giving us all the tickets if we await it but we don't want just all the tickets right we want the cap so we could do is we could select count equals and then we're going to use the SQL helper and we're going to count star perfect and this is going to be a number Jesus I can't type number all right there we go so now if we get um here so this is going to be our unclosed or open count result and then we're also going to do is we're going to const what other information could be useful um how about we do the tickets that were closed in the last seven days so uh closed in fast speak result and so let's so uh copilot's doing this we're actually because remember we're using timestamps so drizzle converts all the dates uh for us so what we're going to do is use and so we're going to use and from drizzle and so what we need to do is first make sure it's closed and then we're going to have greater than and less than and that should be good and so uh drizzle makes this pretty easy right we can just work right in JavaScript with date objects and so then we can get R close and pass week result now something interesting is that these are like separate database queries right like this is going to happen and it's going to fully wait for this to finish and that's going to do this one right like what if we could like these don't depend on each other so what if we could do them at the same time and we can with um batching and so batching is a feature of uh lib squeal which is what terso uses so we can const result equals tenantdb.batch and we're going to provide it an array of queries to make so we're going to get this one put it in here put a comma put this one in here and we can just get rid of these and if we look at our result we need to await that we look at our results let's type script loads come on there we go so now we can destructure this into close tickets in last week and these can be these are an array now so we could further destructure these into here okay so we can say const open ticket count Open tickets account equal zero right if it doesn't exist and this should be what's wrong there we go and then we can do the same thing for in the last week and then we can set something up but let's set up a div here and we're going to give it some Styles let's do you know the classic classic Plex Flex call item Center okay now we're going to make like a div and it's going to I'll go pilot help me out sure that works okay I almost have a H1 let's see what cobala comes up with so each one class equals all right this is something let's see how it looks I don't know how that got messed up okay what's what's a mat about I think we're good okay so let's go back I think we have to restart our server okay this looks pretty bad um but we got we got something so I'm gonna take a moment to clean up the Styles and I'll be right back all right so here we go uh things look a little bit better I didn't make too many changes um so I just added with full here so this is hype full with full so just takes up all the rest of the space that the sidebar doesn't and then in our dashboard so I just have a main uh kind of here which is filling up most of this space and then we have these items that are spaced here and we have a flex container with these I like so the text at the top and the organization name and again make sure to put um safe on these attributes where we're putting user content right and then for these cards I made a little card component right and this is just um a div that has a you know a border and some text and a link and then that looks pretty pretty good right um and these just direct to the tickets page which we haven't implemented yet this one uh it's just a dummy thing um you know we could Implement that uh maybe in a future video I don't know a little bit out of the scope um just for this video and then um these cards so yeah we have a card component and it's in a bit of a grid thing I tried to set it up so it um kind of works out well uh that's not horrible this could be improved but that that could be way worse so um looks semi semi-decent um something I want to do though is uh these like click me's uh are kind of ugly so let's make a fancy link button right that we're gonna put in the bottom right corner here so the first thing we're going to do is go here and let's let's get some classes on here so it's class equals um absolute let's do bottom three right three there we go okay now let's add a little icon okay let's add an icon so let's put let's put this in a span now let's add just like View and then we're going to add a div with class let's have a i blue side Arrow right sure okay that looks good is there like another arrow I was thinking more like up and to the right let's try this one Arrow up right copy that go back here and okay let's get those in line so let's Flex row okay now it's items Center Maybe okay now it's Gap three four gas too much too okay that was too little three okay so now let's add some let's add like uh BG Gray 200 over BG gray let's make it 400. and I see that now is this these um oh these need to be up here oh no those do not need to be up there let's add like uh let's add some padding we're gonna p x through okay sure I definitely need some more X definitely need some more X maybe a little less X okay I like that um let's get some a bit more py is this going off or can I just not tell did I misspell hover let's try making this 300 and then we're also going to text BG it'll say 400. then here hover text PG or gray excuse me ah okay it looks like uh Uno CSS got a little caught up sometimes like like crashes when you're doing the hot reloading a lot so just um yeah the CSS is not working that's why so let's make this much darker let's do like 700 and turn it to 800. okay I like that okay so this is looking pretty good um one other thing we can add that just will you know kind of spice it a little bit is what if this Arrow moved now Tailwind has some cool animation stuff for this and so here's what we do right so we put first of all we put group on the anchor put group on the anchor and that way when we hover even if we don't hover over the arrow we hover over the anchor we can kind of hook into that grouped Behavior so yeah transform transition transform we add a duration and then on the group hover we translate it a little bit up and a little bit to the right and then if we go back look at that wow wow this is pretty cool guys so okay I would call that done for our dashboard great so now what we're going to work on is this organization page and this is going to have a couple things so first it's going to have a way for us to get the um invite code to send to other people who want to join our organization it's going to have the link to the do ticket creation form we can send to our customers and it's going to list all the members of our organization so let's get started with that so we're going to go in here we're going to make a new page we called organizations.tsx so we're going to export const organizations equals new Alicia let's import that or we can just use the contacts first use context and let's see sure I don't even know this did and then we could just do that okay we need to import Alicia let's just have this return um HTML and then we are going to return our base HTML right base HTML then we're going to return our dashboards this is a dashboard view dashboard and we are inside let's just return TBD right TBD we're not TBD whatever TBD so then we're going to add this to here let's just use uh organization okay now we should be able to go back here and go to our organization page this should be non-plural that's why we do this over here let me hear here here enter here here here okay and then we also have to change it in here right about that one okay so now we can go here so what's cool about uh this you'll notice is the browser is not refreshing and that's because of the HX boost right but we can make this even smoother and so what we're going to do is go to our base HTML right and we're going to tell htmx to use view transitions everywhere so that's going to look like this we're just setting the global hdfx config and the global view transitions value to true and just that one line change if we go back to our app oh look at that buttery smooth all right pretty cool so now let's get started on this organization page so first we're going to need to do is if there is not a session we're going to redirect to log in I like it let's import redirect perfect now obviously we're going to get need to get um information about the organization so let's get that organization and then so now that we have the organization we can just extract we're going to get the uh invite code invite code it's going to be your organization Dot see what we got database name right and we're going to hit the const let's see creation ticket creation URL um and so we haven't made this endpoint yet but I'm going to say it's going to be we're actually here we can get our config Dot end dot host URL what is it do we not have config we can extract it from here big.n.host URL and now we can let's see now we don't need this invite code just slash ticket Slash new okay so now let's add to our dashboard how do we want this to look and then finally we're going to get our employees and that's just going to be our users where their organization ID is the same this is a user because Lucia doesn't use plural and that should be good nice all right so now I'm just gonna I'm gonna throw in a bit of a layout here for what this page is going to look like all right so again I apologize if this is a bit jarring I hope it's better than watching me mess around with styles for uh much longer um but I'm just gonna go over through just go through what I added so very similar to the dashboard page um we're using this main with the flex one and then we have a div at the top here um with a manage your organization with your organization name and a little Shadow and that looks like this and the shadow looks really faint uh maybe like Shadow large I just don't think it's there okay don't I'm just not gonna worry about it I maybe the shadow will appear at some point um and next we have uh the things we want people to be able to copy right so we have this employee invite code we have this um create uh ticket link and this is the same as um looks like typescript it got a little mad and we actually don't need to create that because we already created it here ticket creation URL so we could just put that in there perfect and so now um or wait we actually do need to yeah I forgot about this part we need to put the organization ID in there because uh yeah you will see so um now that we've done that we can put in our ticket creation URL in here um and then finally right we have a header for employees and then if there's employees and um there is more than one employee or if there aren't and we have a little you have no employees although you can have one set up um and other than that we can map it and for each one we just have a list item with a div with flex and we have their profile picture which we get from Google right and their name and then we have their name and their email or no email and so if we go to what that looks like we could see I don't I don't know why the the email isn't working um maybe just like a Google thing I don't know you can try to figure it out on your own so uh this looks not half bad um but let's add a little something special right so right now selecting this uh you know it's doable but it's kind of kind of annoying right now something we could add to really enhance the experience on this page is instead of having making users kind of Select these because we can have like a click to copy button right here right and we're actually we're going to need to do some client-side scripting right like there's nothing htmax can do that's going to help us uh make this copied onto our clipboard so what we can use is hyperscript now you may have seen my tweet about this before this video went out it's like the biggest tweet on my profile uh ever and it's it's what we're about to go over um people are are a bit uh mad they're they're they do not like the hyperscript but I think this is actually a perfect use for it right this is something super super minor I think it comes out a little bit cleaner in hyperscript that doesn't JavaScript hyperscript is just to kind of enhance your your htmx site just kind of here and there where you need it it's not meant to do everything so let's see what it would look like to implement a copy button in the hyperscript so the first thing we're going to do is Define a global function so we're going to do that in a script tag of type equals Text slash hyperscript okay and so then in here I'm going to put in some text and she defined a new function with Define and we're going to define a copy selector to clipboard it's going to take in an input of selector and then we're going to get the inner inner HTML of selector I think you could probably put that on there as well but and then we are going to and then we are going to call navigator.clipboard Dot not this right text with the result okay and then we're going to end so what this does it sets up a global hyperscript function we could use um that allows us to we can call it with a CSS selector and it's going to copy the contents of that selector into the clipboard pretty pretty cool so now let's go and Implement what that's going to look like so we can go back to our organization page and we're going to create a new button inside this div so let's create a button let's just give it some Styles we're going to give it uh you know like a PX3 py to a little BG I don't know gray 700 hover Peach let's just yeah there we go copy flash button okay I don't think we need the text white but we can go with that see what that looks like pretty horrible I think we do actually need the text White okay that's looking okay let's add copy to clipboard just to make it a bit bigger and that look there we go that looks okay so now if we click this it doesn't do anything right so let's add our hyperscript okay so we're going to add our underscore attribute I'll just say on click log hi Vlog I right because then if we go to the console we should see here we go pretty cool so now let's try to call our thing here so we're going to give this an ID of let's say org code right org code and so on click we're going to call call org uh there we go I don't think we actually need the quotes as well and so then if we go back okay it's bad let's see navigate oh because I misspelled it look at that Navigator Navigator and then look at that okay so now we don't you don't really know that this happened right like how do we make this a dynamic well let's do that so go back to this button and so um we're going to let's add here so then we're going to set my inner HTML to copied let's see that okay then we're going to then wait one second we're going to set it back so now pretty cool right so now maybe let's um let's change the Styles a little bit so let's then set my class list class list two [Music] here we're gonna put this here and then we're going to copy this line and let's make this um let's make this green let's make it like bright let's make it 500. however 700. so then pretty pretty cool right and so again you're not doing insanely complex logic with hyperscript right like this is very very basic stuff but it just makes it easy it's kind of fun it reads cool right um now another cool thing we can do is if we spam this button um do we get this like kind of flickering Behavior so what we could do is we can on click and we're going to cue none so you can either you could do throttle here or you could do Q none and what Q none is going to do um is basically while the event's going off if the event gets triggered again you can like cue first Q last Q all and q9 just basically ignores it right um and that's pretty cool so now let's Implement that for this other one so let's copy this put this um here and we're going to give this an ID of org URL you can put this here and if we go here copied and look at that pretty pretty slick right all right so that's this page done looking pretty good so now the juice the juice right the tickets let's get to the tickets alright so to get started on making tickets we're gonna need a couple of things so there's two front end endpoints we're gonna need we're going to need the uh Slash organization ID slash ticket Slash new right and that's the form we're going to be able to send to a customer where they can fill out a new ticket and then we're going to have a slash organization ID slash ticket slash ticket ID and that's where people are going to be able to view those created tickets on the back end we're gonna have a couple more stuff but let's just focus on that front end portion for now and then also we're going to have a tickets endpoint um in the dashboard so let's let's just scaffold those out so we're going to make a new file we're going to call this tickets dot TSX and then we're going to export export const tickets equals new Alicia dot use no that's not right not use context and I'd get slash tickets and let's just have it return uh let's have it return let's get here sure and HTML sure What's it gonna do base HTML test okay sure that works right uh gotta do that there we go okay what are we missing one set of parentheses perfect so now we can go to here and we cannot use tickets okay then we should be able to go back to our app and go to tickets uh see why that's not working ah because I did not save that is why all right so now oh we forgot the dashboard let's add that dashboard in there of course let's add that sorry okay here and then we can dashboard dashboard I then we're also going to want to um get let's just copy all of this right there we go and or redirect let's make this async there we go okay Perfect all right so there's our tickets dashboard page now let's make the let's make a new folder and this is going to be organization ID now something nice is uh we can name this whatever we want because the actual routing is done by the Alicia like typed router right um this is just kind of basically a convention for us and so now we're going to create a new folder in here all tickets ticket and in our ticket folder we're going to create a new file we're going to call this dot ID dot TSX and we're going to have new DOT TSX and we're also going to have in we need like a kind of collector file and a collector file for here star.ts um let's just scaffold these out right so we're going to have export const ID it's going to be equal new alicia.use context sure and this is going to have dot git and this is going to be um slash let's get this on a new line let's start right there we go okay it's gonna be slash um org ID ticket slash ticket ID great and then sure click and do all of that it's import Alicia from Alicia and let's just close this for now um and then once we could just copy this in here this is going to be new Slash new um that should be good okay and here we can let's get like that should be pretty simple I'm gonna const ticket equals new Alicia dot use we're going to have ID from that ID and I use new uh oh yeah new is a taking keywords we have to do like new route okay and then we can and here okay there we go and then in here we can just like copy this basically get rid of these and this is going to be called org ID or you can dot use ticket and right we're just merging up the routers and then in here we can dot use org ID I didn't export it const org ID there we go okay all the routes are set up so um let's start with with new right so for this page this is going to be like a public page right like we're going to send this to the customer and they're not going to be signed in so uh we actually won't need the session on this page at all so what we're going to do is we're going to get const organization equals db.query.organization dot find First there we go okay and then what we want to do also in here is params is going to be a t Dot object here perfect so let's get uh let's import this Port T from Alicia and so what this is doing is typing our params so we want the org ID actually this should be a a number actually and that should fix this there we go okay so now if we don't have an organization if not organization all right this is a public page this is an API route so we want to um wrap this in in a base HTML right base HTML so organization not found maybe like uh is the link correct so we should be able to [Music] um right now if we go here we go to slash like tickets new ah so the reason why this is not working is because this actually should be numeric and what this is doing is automatically basically coercing things to a so a bit of an oopsy there um I forgot to export this and this is importing uh not what we want so we should import ticket here and now if we go back here um this won't be found but if we go to not that there and then we huh all right so I just spent a little bit too long uh debugging kind of a goofy issue basically I forgot to await so that's on me um but now if we go to here now look at that organization not found is the link correct perfect so now if there is an organization we want to kind of have a form that's going to submit to that organization's endpoint so let's define what that endpoint is going to look like right so let's create a new controller and this one's going to be called let's see ticket ticket dot DSX we're going to export const ticket controller this new Alicia I use context and let's make a man I hate why this doesn't go into a new line immediately Let's uh dot post all right so we're just going to create a new poster or actually this could be slash because we're going to add a prefix ticket look at that okay perfect we're gonna Define a little async function here and let's define the what we want here so let's have a body um let's import T from Alicia T perfect so let's see what up copilot gave us and remember let's go back to our schema it's been a little bit um to go back to our tenant schema and ticket so we need a subject a description and let's actually we can update these so this could be that default or dot default Dot default open um and then this can be why is this mad and here we can add a dot default function new update perfect let's actually make sure in our chats we have timestamp we have forgot that here too so dot default function new date perfect and we actually don't need to make these aren't like any database migrations right these are just kind of orm level things um so in our tickets table the only thing we actually need is the subject and the description right um and also the organization ID because that's like what database to submit it to so organization ID is going to be a number and the title and description are going to be a string and let's make sure let's have like a uh here let's make this like 40 um and this is going to be like three and then here in the description let's make this like 10 and then 500 sounds good okay so now we can get started on building this out so what are we going to need we're gonna need the database and that's about it for now I think so let's just uh consticket equals wait TV DOT insert tickets that's not the tickets we want I want tickets from our schema we're going to add in dot values uh see what it wants description [Music] ah I'm making a grave error right now but uh let's see if you guys can can think of it um while I do this what am I doing that is not not correct the answer is this right this does not exist what we need to do is we need to const organization right and then we need to const uh tenant DV equals get tenant DB right um and this is going to be a object and we're going to DB name and all token this is prefixed with database I believe and then if not we're not going to throw an error we're going to let's get our set and we're going to set dot status equals let's just say forbidden right then we're going to return return okay cool so now this should be good and intended to be then we're going to replace this with that and let's see what it's mad about here this should be subject subject let's make this here this should be subject and there everyone is happy and if we dot returning or do we need yes we do so let's returning and then this should be the ticket pretty cool okay so what we're going to do now is if that ticket that looks good and then we're going to uh return a redirect right um and so where are we going to redirect to let's make sure we have our headers extracted headers editors and so what we're going to redirect to is the chat page for that ticket so that's going to be slash yeah copilot's got that look at that perfect um and so that's this page right here right cool so here we created a ticket nice and so now let's make the form on the front end right so we're going to have a um so if there's non-organization we're going to turn this otherwise else we're going to return HTML we're going to base HTML let's have like a main class Flex uh Flex call items Center um okay and so then we're going to make a form and this form is going to have a let's just like copy one of our other forms we've made so let's let's use the login form because that one's pretty good another login form the new user form there we go so let's copy let's actually just copy this whole thing right reuse the code now let's go to our new tickets we're going to get rid of that looks good let's hide the console so now let's say like submit a new ticket to okay perfect and so now we can get rid of the second form right and we can just make this error message this can be error message we're going to API slash LC um did I not merge this in that is why let's start use ticket come on there we go now we can go here and if we look at that okay so we can post a ticket perfect and let's say subject this is going to be four subject right and then let's go like confirm our validation here right so we have um 340 right so we came in like three 40 um and let's keep this uh this pattern here um and also I forgot oh no we have safe there that's good and so then in here we can do submit ticket um and then let's add a second input a second label and we can just put those here this is going to be description now let's call this description okay for description and this is actually seeds to be here right okay and then uh your 10 to 500 right so we can do that as well we can actually make this a text area this could be kind of big let's set the rows by default to like five sure and the placeholder is going to be like please describe your issue in detail okay um and enter subject for your ticket okay and let's see what that looks like if we go here and then we copy here that looks pretty good wow man okay so this form is looking real good um so now let's just uh before we test it right we're going to get redirected to this page so let's just kind of give us something here to know that we are on the right path right um so let's get our base HTML base HTML and let's do something like uh let's confirm that b should both be numeric this should be like that and this should be like that there we go import t and then let's just uh P like this on Dot s what is it mad about oh this should be a t dot object there we go okay so hopefully um there's a lot of errors I'm just going to assume that typescript's being wonky okay hopefully uh when we submit this form it is going to create the ticket and redirect us here let's try it out okay so I have a problem um I forgot to fix or I don't know the button doesn't work please help me let's see in a valid body organization ID I think this is because I copy pasted the form so let's go back here let's look in the logs okay invalid property organization ID required property ah okay so we're missing something from the form data we have the organization ID here but for it to be submitted in the form data what we're going to have to do is just kind of add a little hidden input so it's going to be type equals hidden and there we go so copilot knows what we're trying to do basically this isn't something that someone's going to uh to fill out right but it's just there and kind of hidden so that when we submit the form it gets posted along with the other form data right so now we try it again I have a problem the button doesn't work please fix and submit look at that that was that's a pretty good DX I like that that loading center right cool okay so now let's work on uh this page right uh so first let's just kind of commit our changes so far so we're going to say added uh submit new ticket page looks good uh so on the page for the ticket we're actually going to have one page for both the employee and the user and how that's going to work is basically if you're signed in and you go to this route it's going to give you like the employee View and when you send messages they're going to be employee messages and if you just view it as someone who's not signed in uh you're going to be kind of sending the customer aside the messages um and then we're also just going to you know make sure to check that if you are signed in you're actually uh unemployed of the organization for this ticket um so let's Implement that so uh first let's get the const organization look at that perfect and then um that looks fine okay and then we can uh we need to compare the session right so um uh cons session org ID equals session dot user.organization ID so actually here we're just gonna have let's have like I'm just gonna make this on here function chat page this is just going to be a placeholder for now look at that perfect okay um so and then if basically uh this is important and we do need to get the um the ticket as well and make sure there's a ticket if there's not a ticket uh here um and the other thing we need to do here is right this is the the ticket is in the data so we need to get the cons tenant DB equals get tenant database tenant DB it's going to be there we go and then here goes here okay oh let's see is this DB name okay here's our ticket if we don't have the ticket ticket not found okay so now um at this point we know it's a valid ticket we know it's a valid organization right so we're going to have basically we're going to return uh based on the the session right so if session session but first let's compute like like if first if session dot user dot business not business or organization ID if it's equal um to the organization ID or not equal not equal if it's not equal um here we can okay so if the session exists uh so this is fine I was gonna redirect them um I don't like this right because we're actually allowing technically anyone to have access so we're just going to in this case we're going to redirect redirect um and not two there we're going to redirect to just slash dashboard right so if you're like looking at someone else's ticket we're just going to send you straight to the dashboard looks good um so now we're going to do this part based on the session so if there's a session we're just going to do like this or this and basically let's put in so if there's a session what we're going to do is the dashboard dashboard uh chat page dashboard and otherwise we're just going to do the um epitch right perfect great and so now if we check it out we should see the chat page placeholder right Perfect all right cool so now that we have a placeholder for this chat page I'm actually going to kind of put a pause on this and go back to our tickets page right because now that we can make tickets let's view the tickets so let's uh go to our tickets page and we're gonna get the tickets so we're going to const uh tenant DB equals get tenantdb looks good now we're going to uh looks good um that's that's fine all right so what is it mad about uh where tickets dots we actually don't need yeah we're just getting them all just find many right and again we don't need to sort by org ID because we're searching an organization's database cool so um let's set up a main in here and so what we want to have basically is uh kind of like a header and then a little box for each ticket which is just going to display like the subject and a little blurb of the description and the status so I'm going to kind of scaffold that out and show you what it looks like all right so here's what I laid out um got a main with uh this Flex one and some some space right and so then we have a kind of header area here this is manage your tickets and then it just has the organization name and there and then the shadow which again uh isn't working or it's like super faint I can't really tell um and then for each of our tickets so if we have um tickets at all or the tickets is zero then we're gonna have you have no Open tickets um let's actually just we're not really filtering you just have no tickets period right awesome um and then we're going to map over each ticket so I set out a little like grid calls thing um and so I just Json stringified it for now but let's create a a ticket card uh component so let's um ticket to get card and where a ticket equals ticket perfect and then this ticket card is going to have a link to that ticket so we're also going to need to pass the organization ID because remember this ticket actually doesn't know what organization it's in so we're also going to need to pass um org ID equals work ID is that in scope already okay so now let's make this ticket card so function uh ticket card so it's going to take a ticket and a org ID as props and the ticket is going to be of type ticket from our schema and org ID is going to be a number already okay perfect and so now we're gonna return a say a div for now [Music] um okay looks all good so now in this div so let's create another div uh class that's going to be Flex that looks fine I don't think we need this right um this needs and equals let's create this div and in here we're going to have that looks decent so if we go here okay that doesn't look very good so let's let's make this font bold okay then sure and then for this status thing I have some Styles we can we can copy in um let's just make the quotes right on there and look at that and so let's actually adjust those styles based on the status so let's do uh here open or close so if it's um open we're going to set the status to Yellow let's just uh do the same for both right now all right and then if it's closed which is here we're going to set these both to Green all right so now let's give this outer div let's give this a a border like this rounded medium and let's give it a shadow large okay that's pretty bad uh so let's give it some padding how about a little bit of padding looking pretty good so let's add the description in there so below this if let's add a p and we're going to have a class say text Gray I'm going to say 500 500 and here we're gonna put in safe we're gonna put in our to get that description so that looks that looks pretty good looking a lot better um let's make this a little bit bigger um let's just get rid of this how does that look um looks good cool so this is looking pretty good now what I want to do is in the corner um I have these like kind of fancy little like button links right like like I have um here so what we're going to do is go to our dashboard page let's extract this into its own component where is it um here this right so let's make a um let's put this in the components I'll just have a index.tsx and we're going to export function I'm going to call this fancy link and this is going to have a text and an href and I think that's it um so let's copy this and throw it in here hey traffic so href this will be the text let's make sure this is safe then here we can replace uh this right with a fancy link component fancy link if we import it go back okay let me go here reload and still looking perfect so now we can go to tickets and import our fancy Link in there so let's get a fancy link and looks pretty good uh why not why not working uh so the reason this isn't working is because this is absolute so what we need to do is um in this div we need to slap on relative and there we go okay it's a little bit big all right so this looks a little wonky I think it just leads to let's make the card a little bit bigger so let's go back here and let's set the height let's make the height uh 20. that's not good all right let's try 40. that looks fine and let's just get rid of these and I kind of like that um it's there's like a little bit of space here but if the description is longer that'll that'll fill that out so that's fine and let's actually test um making uh some more tickets right so let's make a I have another problem this one is real I swear I could submit this okay so we reload so let's actually ah you know what we can put in here we can put the date right so let's do this and let's do ticket dot um created at a two Locale string I like that let's make this um text large there we go um that looks good that looks good so now Let's uh make let's make one more for good measure so let's go back here let's say um the server is down I can't connect and it 404s please there we go cool okay this looks pretty good um let's actually let's add one more just in case we messed up the uh the grid styles does grid fix things please work and looks good now um we want the most recent ones to be at the top so let's just adjust I think that's um outward uh doing this so let's I think we want to order not because we need to order by this is going to take tickets and to take that looks maybe ascending I think ascending there we go Okay so um or wait we had it right the first time there we go okay the most recent ones are at the top cool this is looking pretty pretty cool so now we're almost done now we just got to figure out this chat page right how do we how do we figure out how to talk to our customers and get our customers to talk back to us all right so we're on the home stretch let's finish this chat page and call it a day all right so let's get started um so I'm gonna start by making just this big div let's say I'm gonna say flex and height uh screen and flex call Grand equals there uh let's see that looks fine so now um what I'm imagining here is let's have like a little header at the top here and then on the top right is where we're going to distinguish either it's going to say like customer view or employee View and that's how I was apart from whether the dashboard is there or not let's actually let's just um see what that looks like uh if we like slash one slash ticket slash four why do we get redirected to login um here so this should be if session and right so if there's a session but if there's not a session it's fine so we can go back here slash ticket slash one slash tickets slash four okay great so now we can see just the whole thing from the customer View um but actually yeah let's let's sign in again just to sign in and here alrighty so all right so let's um the two things that are going to determine the layout of our chat page is whether we're an employee or not and the ticket itself so let's make those props here so we're going to add ticket and employee right and so the type of those is going to be ticket going to be ticket uh from there and this is going to be why is it saying any employee it's going to be an optional Boolean right okay so now here um we're gonna have let's make that header at the top so we're going to make a new div all right and we're going to give this div some style so let's give it a little bit of a border on the bottom border Gray 300 how's that look um oh we can't see it because there's no content in there that's fine for now um so border gray and then I almost give it a little bit of padding all right then um to kind of arrange the content in the middle and then flex and items Center and justify between all right and so then we're going to have two divs we're going to have a div and this is going to be like the the right the content on the left um and we're going to have a div that's going to be the content on the right so all right so in here we're going to make a new H1 and we're going to uh we're going to say like subject uh it's going to be ticket.subject let's make sure this is safe um and then we're going to add a Span in here span and this is going to be based on the ticket dot status let's see how that looks um and that's because we're missing the props here so let's add the the ticket and this is actually just going to be um true and otherwise here we can just put the ticket okay looks good and so if we go back here um that's not right so that's because um safe kind of escapes everything in it so we need to put this in a span as well um we can just drag this here Um this can be safe and then what we're going to want to do is apply some Styles obviously um so first of all let's make this uh let's say like text I don't know 3XL font bold Flex yeah I'll just see that okay let's add the if you remember from the ticket page uh we have this thing so let's just actually copy this not here let's go back here and just throw that in here so we can actually get rid of this and that looks okay um so let's I think we do actually want item Center item Center I gotta save and then let's let's gap for maybe okay that looks fine so um on the right side we're gonna have based on the employer or first we're going to put the the organization organization that's going to be the ticket.org we should have I guess we need to pass the organization name as well so let's make that a organization name we can make that a string and then we can update those here and here okay and so then we can get the organization name again make this safe look here that looks fine we needed to make this bigger for sure so let's add text maybe 2XL and if there's an employee so let's add like a employee and employee that looks fine or let's actually make this a ternary and we can or customer okay let's make this a bit lighter Maybe text Gray 600. it's fine and so now let's actually put this in a a wrapper div that's going to be class Flex Flex call items end and the reason we're going to do that is because we're going to put an anchor tag here this is going to be like looking for employee View and we're going to actually only include this if it's um not employee not employee and here um and let's uh let's style that up so first we're going to have the href let's say going to be slash login let's actually make sure that that is right um here we're gonna want let's just send them to the uh I'll send them to the login page and let's add some Styles let's make this uh class let's say text blue Maybe 400 hover underline um and let's sign out to see what that would look like um I think that could be a bit darker Maybe 600. okay and that is not working because um let's just direct people to here so they can sign in there we go all right so now let's go back to the chat View and so now we're gonna need to have the chats and then the input at the bottom so let's make that all right so we're going to make a new div and this div is going to have a ID of chat messages and this is actually going to be kind of important because uh this is where we're going to tell htmx like where to swap in the new messages and let's add some styles to this um this looks fine let's make sure that didn't break anything too too bad okay foreign bubble say chat bubble component and it's going to take a chat which is going to be of chat chat which I don't think we ever exported so let's do that export type chat equals type of chats.infer select and so we can go here and that should be available perfect and now we can um let's just for now let's we have like a timestamp and a message for now let's just put a div safe um with the chat that message just for now and so then in here right so we actually need to get the chats so let's go up here in our select and let's with then include the the chats that's true and now we can ask for those as another prop so we can get here and then chat should we chat array and the section needs to be there and now we can add these in here tickets at chats and chat's looking good all right so in here we can chats and that looks good um so we don't have any chats right now so this shouldn't have anything appear um uh we need to return to return this and that should be fine okay so there's our chats and you can see I added overflow y Auto so Windows overflow they should scroll and we'll test that in a second once we actually have some chats to test um so now at the bottom we need to have is a div and this is going to contain um all the stuff at the bottom uh so what is that going to be it's going to have that looks fine so let's see what this looks like um okay wow that looks pretty good so now Let's uh I don't think we need an ID for this so this is going to have a name it's going to be message right and so then we're also going to have a hidden input input I'm just going to be type equals hidden um the value is not going to be the ticket ID it's going to be the um is employee is employee its value is going to be employee um we're just going to say yeah that should be good um I'll just say boolean.2 string is that false does that work that should be good okay um so let's just look at what uh this gave us all right this all looks good um let's put Like A Min length one maybe required definitely um okay this looks fine let's make this uh gray 700 to kind of match the rest of our our theme Here um looks good okay so now and let's Also let's give this a hover all right like hover I don't know why this connection's really slow they're pretty good okay that looks good um let's actually let's swap these I like that okay and let's make this um quiet equals true sure um on here we're gonna have a like uh Focus Gray that looks fine all right so uh now let's set up the endpoint right so we're going to have uh well it's also let's add a error message below the form so this is going to be like uh the other error messages we've done so let's just copy that here go back to uh chat it's gonna be here error message um so let's get this and then also we're going to data loading States and we're going to um the button is going to be data loading disable D um that's good and so now we're going to HX post to slash API slash chat um that endpoint doesn't exist yet so this should error and we're going to HX Target chat messages I'm going to HX swap those uh before and and so we're going to do is it's when we post a message it's going to give us a new chat bubble back and we are going to append that right to the bottom of this perfect so um we actually are going to need to export this or let's actually let's move all of this um or we can leave it here for now so we can move this all to the components folder I'm just going to leave it here because we already have all the Imports and this is like the only place it's being used besides that one endpoint on the back so this is fine um all right so uh and then the other thing we need is HX Target right HX Target on um 4xx and 5xx and then we're also going to need to enable then the if we look at what we've done for new new user need to enable the HX extension response targets right so um let's just slap that on there cool so let's make um let's make that slash API chat endpoint so let's make a new controller it's gonna be called uh chat.tsx and we're going to let's just like copy this here um call this chat controller chat um we can this is fine I don't think we'll need this and we can so what are we what do we need to know right we need to know the organization ID we need to know the ticket ID and we need to know the message so let's do that um so then also in here we can set a max length to 500 as well um so now that looks good so we need to we have the message we have the uh and then we also yeah we need the his employee is employee to that Boolean let's I think this is going to be a uh uh I don't know if that actually gets json's I don't know we're gonna see we're gonna see so let's actually right here here's what we're gonna do we're gonna employ true or false and then we can just in here is there like an enum true [Music] does that work no um I think we can do like a TDOT Union uh that's going to take a array of and these should be in quotes and that should be good that should be good if we look at body there we go okay so let's make sure we have all that here so we have we have the message right I have the message we have the is employee so now we need to add the ticket ID and the organization ID so we're going to add two more hidden inputs and this is going to be the organization ID it's going to be the ticket ID and this value is going to be not that organization uh or can we just get the entire organization I think that could be arranged and then if we go up to where we use it we'll need to pass and we can just copy that put it there all right now um here we're going to organization Dot name and down here we're going to organization dot id.2 string that's a number and these are numeric good good so now this is going to be that does that work okay sure cool so all right so now we have this and let's [Music] um let's hook up this controller so we're gonna dot use and this is going to be our chat controller that should cause this error to go away all right so now what do we want to do in this um controller right so we need to get the organization we need to get the ticket uh we actually don't need to get the ticket per set or we do we do we do and the reason we need to get the ticket is because we have to update the ticket um time the updated that time right so so we don't need to get the ticket but we need to update the ticket so we have our tenant DB so what do we need to do let's [Music] um Let's uh await tenant DB dot um insert we're going to create a new um chats we're going to dot values what does it want um once a message and a sender um so we can actually let's switch this around we're gonna make this like roll and this is going to be employee or customer and then we should just be able to directly assign this um was it not like ah I spelled it wrong how do you am I stupid I am stupid okay or wait wait I'm I'm really dumb wow okay um this is not how you spell customer but I don't want to rename the database column so that's just that's what it's going to be it's okay though um so now this should be good right um so mad about now the ticket ID of course ticket ID buy ticket ID right nice okay cool cool so we're inserting the chat and then now we need to wait um dot update and we're going to update uh tickets um and we're going to did I import that right update that tickets.set um updated at new date and uh where that looks good see what's it mad about um I think we need to import from drizzle import equal s RM and that is good okay so now and so now the final thing is we're going to return we're going to return a chat bubble with the uh it won't say chat right so we need to actually return this so const chat equals dot returning and then if um if not chat I'll just say internal server error and we're going to return um internal server error and then otherwise we can return a chat over the chat let's do the chat and we need to import this make sure that's exported that is it okay we're good so now um we should be good now I'm going to batch these into one call you know just for uh a little bit of performance increase so let's um cons to result equals 08 10 and TB dot batch and we're going to pass it an array and so let's put all of these in there so we're going to have this then a comma and then this and so we can get rid of that get rid of this and so the result is going to have um let's say chat yeah okay is that right that is perfect okay so now when we send this form in we should get a chat bubble back so let's start up our server and see if that is the case let's say hi and validation let's see what happened because we never updated the form to match the new format so here we're going to say employee employee or customer right misspelled customer perfect so let's do that let's say hi see what's wrong now invalid body roll because this should be wrong okay finally one more time let's say hi and look at that all right so that's pretty cool um so now we just need to do a couple things so usually in the chat you would always have whoever's kind of speaking on the right and the other person on the left and we could definitely Implement that but just for Simplicity we're going to have the customer always be on the right and the employee messages always be on the left so let's look at how we're going to implement that so I don't want to go too deeply into the Styles here I have a chat bubble component kind of prepared so I'm just going to throw that in here so here's what the final chat component looks like so basically we apply some Styles in our flexbox so if it's an employee we're going to justify at the start otherwise we're going to justify at the end and that's going to determine kind of whether the bubble appears on the right or the left and then we have some colors being determined uh whether again we're an employee or not because we have this chat message here we're going to want to make sure we apply the safe attribute as well and then I just have the timestamp there and this looks like that because we want to apply safe not on that but on this so we're going to put this in a span with say and move this inside there and now this looks pretty good um two Locale string is probably not ideal I know there's some date formatting libraries out there um so there's some other stuff you could try but this this looks fine for now um and then let's just confirm if we open a new window and send some messages here I am a customer they appear on the right cool so now um there's two things that or first of all let's let's see how the scroll Behavior ends up so let's just like send a bunch of messages oh I can't even I can't tell but I was sending messages but basically it doesn't um automatically scroll like if we refresh the page we want to see the latest messages right but they're showing the the earliest um and then also this form doesn't reset when we send a message so let's address those two things and we're going to use hyperscript for both of them um so the reset is actually super easy we do this a good amount with hyperscript so we're just going to underscore and then on submit uh me Dot reset I think and let's try that out test there we go that's pretty easy the scroll thing is going to be a little bit trickier so the way we control the kind of scroll behavior of an HTML attribute is usually with JavaScript so we go into the Dom we get that element and they it has uh two properties that we care about so we have the scroll height and the scroll top and if we wanted to scroll all the way to the bottom we set the scroll top equal to the scroll height so how do we do that in hyperscript so let's first let's create a new because this is the div that we want to scroll right so we're going to create a new element and is that a thing you can do scroll to bottom yeah I don't I don't think so but if that was a thing that would be pretty helpful but what I think we can do is we can set my scroll height or top my scroll top two my scroll I I think this will work let's try it yeah look at that pretty pretty cool um so I think that will work and we have that going on load now it does kind of have a little bit of a flicker so um not amazing but it's fine um and then if we type in a message now you can see it still doesn't scroll but what we can do is um now there's some htmx attributes you can listen for um so you should be able to do like on htmx after settle is the one I think we're um and also we want to put end here so it knows that's the end of that one it's on hmx after settle test uh wow there we go that was it so um now you can see when we load the page it Scrolls down and then when we send I I am the employee there we go and that's actually I kind of like the the darker um being the employee let's let's swap these colors around and say customer again I know it's misspelled there we go I'm the employee how are you look at that and again super super smooth with the view transitions um so how can we improve this let's add a placeholder in the message field so let's add a placeholder because I spelled that wrong there we go okay this is looking pretty good um so if we look at some other ones right tests hello nice really coming along Okay so man I love these I love these if you transitions so much they just they feel so smooth so the final thing is we have all these tickets let's mark them as close right let's let's close these tickets so we're going to add a close button and we're going to do that only for employees right so let's add that up here so let's say um in here and so let's add it uh like at the end [Music] um so let's put all of this in a let's put this in a div I always have like a button just for now this is going to say um close take let's see what that looks like um I was more thinking can we class equals Flex Flex row yeah that's what I was thinking and then we can um items Center and then flexor uh Gap three there you go let's make that Gap even even larger okay now let's make this um let's get this button and let's let's style this one so we're going to skip it um you know some some PX and I'll just yeah let's do whatever uh there um let's make this like uh with these so this um the open is is yellow and then I think we have if we go into tickets I think we have it as green right yeah Green so let's make this the same so if where were we here so if um uh uh let's add let's make this a literal um I know there are better ways to do this with uh was it clsx but this is just this is fine for now um now the hover we also need to um let's cover green green and here we can red okay this is um there we go wait we want this to be uh yellow right yellow so when we close the ticket we need to send a API request so let's decide what that's going to look like um but the first thing we're going to do is if the ticket um is closed we're going to disable the form right so disabled um so disabled equals to uh ticket Dot status equal to close true or undefined that works for me and that way we can't send messages and let's actually let's do that for the um false sat fine with it no um I think this needs to be or we can just I think we can just do this and it's going to be a happy right yeah there we go okay so we can add this onto our message input as well um so now and then also we're going to add a little like Banner at the bottom of the messages if it's closed so that's going to look like um tickets dot status or tickets dot status equal to closed and that's fine for now I have some Styles here just to make sure this looks good we're gonna say this ticket was closed at um ticket updated at the close at 2 Locale string perfect okay so we can't see what that looks like now because we can't close the ticket but let's or was it mad about let's um do a check for this or let's just say never um okay ticket uh that closed there we go okay so now let's create the um the endpoint that are close button up here is going to submit to all right so on the back end we're going to go to our ticket controller and we're going to create uh two new endpoints we're going to create a dot post um slash open and a DOT post slash close um and what information do we need we need the ticket ID and the organization ID right so let's um copy this so let's just kind of have a default Handler for now um and body is kind of an organization ID and a ticket ID that looks good all right so we can actually we're going to do something very similar for close we can just start with open so now in here um in our close button that's not our close button here uh this is going to HX post to um this is going to be uh foreign there we go um that should be happy uh yep okay that's fine um that's mad because we don't have uh this so I'm just gonna make this a blank one for now we can do that there okay should be good now um and then uh this isn't in a form so the way we're going to provide the values is with the HX values attribute and this is basically going to ask for not this it's going to ask for like Json um so we're going to uh it needs to be like there we go and this needs to be these are numbers so this should be fine um and I think that is good so let's go back to our endpoint and we're going to copy this code we're going to get the organization and get the ddb and get the ticket [Music] um and this should be good so we can do that here let's just copy all this stuff into our context um I guess I'm just doing a closed first it doesn't really matter because they're basically going to be the same so we can um get the organization this needs to be an async function you can get the organization if it's not an organization okay and then we're going to get the ticket and we're actually going to um update it so we're going to update ticket we're going to that's set um status it's going to be close and closed at um new date and then also I forgot in our chat one um okay yeah I make sure to set the updated that's good okay so now we're going to get uh we have the ticket and I think that is fine so now we're going to um oh we want to make this we need this to be aware that where that's actually really important um and let's let's make sure we have a where peer oopsies no mistake so let's add a wear there oh wait this we don't need to wear because we're inserting I'm stupid I'm stupid I'm stupid so here um so we're going to get the new ticket and then what we're going to do is because basically the current page uh is now kind of out of date we're just gonna refresh and um because there's a lot of content to kind of swap right like the things that uh like this needs to swap this needs to swap the banner needs to swap in so we're just going to redirect um I'm going to redirect with the set and headers and to that page right um let's see ticket possibly undefined say if not ticket that's just fine for now um oh it's really mad see now if we close look at that wow okay cool so now let's do the exact same thing um for open so we're just going to copy all of this pretty much um and throw it in here but we're going to set this to open and we're going to set closed at to null and we're going to set updated at we're going to leave that for the chats and then this should be pretty much good I think so if we um that shouldn't say close ticket that should say um here there we go okay so now we can reopen the ticket close the ticket and if we go to our tickets look at that pretty cool and so here we can send a message hello sir and let's say they're gonna open it up and respond and say I am mad at you and then we're gonna go back to the employee view refresh and so we can mark that as close and I think that's just about calling it done now it's not done until we deploy so let's figure out how we're going to deploy now deploying with the BET stack is super easy first of all let's let's push we're going to say up done so let's commit um and now we're going to deploy with fly so deploying with fly super easy we're going to do fly launch and I'm going to give this a name and I'm going to call it Beth um bath support that's support I'm just going to put it in Ord and okay so now we need to fly Secrets set um all of our secrets so we're going to set node end to production actually let's just before we do that Let's test to make sure nothing is broken so let's set this to production and let's um do buns start instead of bondev and let's go back and just make sure everything still works okay um that looks pretty good all right so let's um go back here and we're going to fly Secrets set and we have to do this for all of ours so we're going to set this to production and we're going to set the blog and I think you can actually do this in the um in the fly Secrets new secret okay this is going to be a lot faster so we didn't end up actually using the logger a lot but it is there if you need it so that's just um pretty helpful to have so we can set this up local replica database URL database auth token and then we're going to get our Google our Google client Secret and for our host URL you see now we're going to set this to um here this link so let's then set our ocrl to that now we're going to set our turso API key is what we need to do now is go to our Google Cloud right and go to our API access and update our credentials to support our new um URL and it doesn't want a slash this we need the correct callback and that should be good so now we're all ready to deploy so let's fly deploy and it's going to take a little bit and while it deploys I'm going to talk about the scalability so if you know anything about fly you know that they allow you to if we just go to their their home page here fly allows you to deploy your apps basically anywhere on the edge and scale they're kind of like a mixture between serverless and server full it's really cool because they allow you to have full long-running servers but you can scale down to zero and scale up to pretty much infinite which is a really cool balance obviously it has some trade-offs but what we can do is we can create our machines and we can scale them we can scale up the number of machines we can scale up the performance of machines and we can scale up to different locations and again with torso um you can also put a database in all the locations you put your compute in and now again you can even put an embedded database in there so really really cool uh scalability so once this finishes deploying let's actually we can check it out um and look at that we are what's in an invalid request oops let's see what's wrong then all right so I think I had a little bit of a mistake with my redirect URL so make sure that's right in your Google console but I'm now on the live site right and everything works just the same and we can go and we can reopen a ticket and say is everything still okay and close it and everything looks good now so I promise to show you how to scale and it's super super easy so starting with fly you can scale to multiple machines bigger machines many locations um so that looks like this we're gonna fly scale count one so this means we're just going to add one machine and we're going to set this in the region of nrt so that's in Tokyo and it's going to ask if we're okay and it's going to just do it and so then for turso so let's list rdbs um so now again if you're using turso after this video comes out all your DBS are going to be in one single group so you'll be able to scale it to up to two other locations um no problem now I'm working it with it on a kind of pre-release so my main DB and my other DB are in a different Group which technically classifies as different DBS so I can only um so they're being replicated separately but again in the future you'll be able to just they'll all be in one group and you'll be able to replicate that group um so for now I'm just going to show you how easy it is so um with turso you can uh turso DB replicate replicate Beth nrt and just like that we are in Japan so now we have a server running in Japan we have a database running in Japan and uh we could scale to even more now so this technology is super super cool and really really powerful all right so the last thing I wanted to show you guys was I was talking about how easy it is to give a customer kind of organization their data when they ask for it because everything is kept separate and so I'm going to show you what that would look like so we're going to go in and we're going to turso DB list and you could get this either from your primary database or I'm just going to copy it here so we're going to get this database and we're going to turso DB shell this database and we're going to put in the dot dump command we're just going to put that into a org.data file and in a second here we are going to have all of their data these are all the chats and all the tickets and we can just send this straight to the company super super easy right very cool feature of having kind of one database per organization per user Etc and so that's pretty much it I really hope you guys enjoyed this video I put a ton of time into making it if you have any questions I'm opening a Discord server so the link to that's going to be down below and of course all the code is going to be on GitHub you can of course open an issue please feel free to contribute to either the the repo or the template or the CLI definitely I would be totally appreciated again thank you to turso for supporting the channel their features are really really cool it's really fun making an app around it and definitely you should go check them out there's gonna be a link in the description for that and again really thank you to all the support the original Beth Stack video has like over 150 000 views now which is absolutely insane so it's been really really amazing to kind of see the support and now build like a much larger app with it and other than that I will see you guys in the next video bye
Info
Channel: Ethan Niser
Views: 45,676
Rating: undefined out of 5
Keywords:
Id: NZpPMlSAez0
Channel Id: undefined
Length: 245min 23sec (14723 seconds)
Published: Thu Sep 28 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.