In this chapter, we're going to go ahead and implement the multi-tenant architecture into our application. But just before we do that, I want to fix two bugs that I've noticed in the last chapter. One is in regards to the drop-down menu position getting lost, and the other one is something I've noticed in my personal code review, which is that we actually do some incorrect refetching here. And then we're going to discuss how we're going to actually implement multi-tenants here. So as always, ensure that you're on your master branch or your main branch, and ensure that you are up to date.
After you have confirmed that, go ahead and run bun run dev. So the first thing that we have to fix is our drop-down issue. We never noticed this before because we could never scroll before, right? When I look at it like this, it looks fine. But if I keep this open and scroll, this happens.
The position gets all messed up. And there's actually a super easy fix. So easy that I'm almost embarrassed about what we actually implemented. Let's go inside of our Home UI components search filters categories. And let's go inside of the category dropdown component.
In here, we have the use dropdown position hook. We can completely get rid of it. We don't need it at all. And we can remove this, the dropdown position, which also means that we can remove the position prop in the subcategory menu. So now go inside of subcategory menu and remove position from here and remove position from here.
Instead of fixed, we will use absolute. And we're just going to keep this right here and keep this like that. And the absolute is important because our category drop-down wrapper needs to be relative, right? So just ensure that you have this div relative because that's where we render the absolute one. And if you try now, it's still not perfect.
So instead of this, let's try 100%. There we go. Yes, my apologies, not zero, 100%. And once you do that, you will be able to scroll while these are open and there should be no problems whatsoever. So obviously now, we beg the question, why did I implement useDropdown in the first place?
I think I implemented it because in the beginning, I didn't have the responsive solution that I have now, right? So right now, I have the responsive solution here with the view all and my elements, you know, collapse and expand. But I think initially this was scrollable perhaps. And then I wanted to do some calculation to make sure it's all visible. But looks like CSS is enough for this, right?
You don't even have to put it in style you can put it in here if you want to but yeah that's the first fix let's get that out of the way that was the easy one now let's do the incorrect prefetching so this is also very easy to fix Basically, inside of our app, home, category, and subcategory, we do prefetch query, which is not correct because in here we do infinite query. When you do infinite query, you also need to prefetch the infinite query. And now we have to modify this and give it some proper values inside. I think that the only thing we actually need to pass here... So I'm going to put the filters first, the category, and then I will just add the limit to the default limit.
And looks like I still need to do something here. So this is a new syntax for me, right? This is the new trpc integration. So it could be that something else is missing. And I think that instead of query options, I have to use infinite query options.
There we go. Now we get rid of the errors. And now we have to do the same thing in the subcategory here. Instead of prefetch query, it is prefetch infinite query, and instead of query options, it is infinite query options. And let's also reverse these two, add a limit to the default limit.
There we go. So now we will be prefetching exactly what we will be suspending. So if you make a mistake and you don't match what you prefetch and what you suspense, it will simply fall back to client-side fetching. It's just gonna fall back. It's almost as if you didn't do the prefetch at all.
That's how it's going to work. Great. Those are the two issues that I wanted us to resolve. We fixed some bugs. Now let's go ahead and let's add the tenants collection.
So why are we adding multi-tenancy now? Well, I don't know if you have noticed, right? But right now, no matter with what account I log in, for example, I think I have, Do I have John Doe? John, johndemo.com, demo. All right, I do.
So let's try this. Johndemo.com, demo. No matter if I'm logging in with John or if I'm logged in with Antonio, if I go to my admin dashboard and I go into products, I can see all products and we want to change that. We want each user to become a tenant, which we are internally going to know it's a shop, right? So let's go ahead and do a new collection called tenants.
So each product can belong to a single tenant. And then when a tenant visits the admin dashboard, it will immediately detect that they are a tenant thanks to the plugin, which we are going to add. And it's only going to be see, they will only be able to see the products that they have created. So that's our multi-tenant database structure that we're going to build. So let's copy the users collection.
Let's call this tenants, slug will be tenants like this. And now let's go ahead and modify it here. So use as title here will be slug. The fields will be the following. So each tenant will have a name which does not have to be unique but it has to be required and let's add a label for this to be store name so that user understands what this is right and the description for the admin when you see admin this basically means for the person who is in the dashboard, don't get confused by the word admin in this case.
In this context, what admin means is the person looking at the dashboard. So the person looking at the dashboard will see this is the name of the store. For example, Antonio's store. This doesn't matter. Whatever you type here is what we are going to display on your page.
Now the second thing that we need here, the second field, will be the slug, which is a type of text. But it's going to be a little bit different. It's gonna have three properties here, index, required, and unique. And that is because this will be the actual subdomain for the store. So slug.gumroad.com or in our case, funroad.com.
So this is a made up website now, right? Make sure you put index required and unique. I'm going to be calling it slug. If you want to, you can call it subdomain, right? I just want to call it slug.
It kind of makes sense for me that way. Now what we're going to do is we're going to add a relation to the media so that each store can have like an image, right? And then we're going to add the property StripeAccountID. This will be something that we're going to use to ensure that the person who owns this tenant, which basically translate to the person that owns this store, has verified their details with Stripe. And then we're gonna have this build, Stripe details submitted, which is this type of checkbox, which is going to be used to give us information whether they have actually successfully passed that verification or not.
So when you see checkbox, this basically means a type of field, which is a Boolean. So once this is set to true, we're going to know, okay, this person can now create products. Until this is set to true, we will see that you cannot create products until you submit your Stripe details. At the moment, we are not having this logic, but we will have it later. I just want to prepare the fields already.
So Stripe details submitted, Stripe account ID, image, slug, name. Those are the fields that we need. Excellent. So now let's go ahead and let's go inside of payload config here, and let's add the tenants, from collection tenants. But just by introducing the tenants, not much will be added here.
You can see that I can now create tenants. Great, right? But that isn't what we want. What we need now is the multi-tenant plugin. So this plugin sets up multi-tenancy for our applications within the admin panel.
It does so by adding a tenant field to all specified collections. Your front-end application can then query data by tenant. That is exactly what we need. So whenever you see tenant, read store in your mind or a shop in your mind. That's what tenant will be in our application.
So in order To do that, we have to install this. And one very important thing, all of your versions for payload need to be the same. You can see the versions that I have here. So be careful when adding. Bonn Add, and then I'm just going to expand so you can see.
Let me see. 3.33.0. That's the version that I need. If you want to, you can use the latest, but then just confirm that you changed all of the other ones to latest as well. Great.
Once you have added this, we're going to go ahead and just take a peek to see what we need to do. So we just created our tenants slug. So this is great. Okay. We didn't have to modify this, but we are now gonna have to add the multi-tenant plugin.
So let's go inside of payload.config.ds. So inside of the config, we're now going to add the multi-tenant plugin, which we just installed. And now we're going to add it here in the plugins part. So let's go ahead and add it. And then inside of here, we have to define the collection, which will be tied to the tenant.
In our case, that's going to be products. So each product will be tied to the tenant. And then I want to add some additional options here. Tenants array field, include default field will be set to false. And one more thing, user has access to all tenants.
So this has to be a Boolean, right? True or false. But in our case, we're going to go ahead and do the following. We're going to get the user from here because it's a function. And then we're going to do boolean.
If user?roles includes superadmin. Now you might be confused. What are roles here? Well, that's something that we have to do, which I forgot, right? We need to add superadmin to user rules.
Well, I didn't actually forget. It's the next in line. So let's quickly do that as well. We have to go inside of our users collection. And besides the username field, we are now gonna go ahead and add the roles field like this name roles type select default value user as many true options super has many true options, super dash admin, or just a normal user, like this.
And let's do one more thing. Admin position sidebar, just so we separate it from the other fields in the form. So now it makes more sense, right? Because now our user can have the rules. And my apologies, did I call it roles?
There we go, so roles is the name. And I think that we should get the type here, or maybe not, maybe it's going to stay at any, but just make sure that you have the roles here and that you have the super admin here as well. Great, so let's see what we've done so far. I think that right now, this won't just magically start working, right? Because we haven't defined any roles, like things are still in some weird state.
We're gonna have to refresh our data. But let me just see. So we added the tenants collection, we added multi-tenant plugin, and we have created super admin roles. Now we have to go ahead and add the tenant field to the user. So the way we do that is by using the multi-tenant plugin inside of the users collection.
So let's go ahead and import tenants array field from payload CMS plugin multi-tenant slash fields. So what we're going to do now is the following. Const default tenant array field will be tenants array field, and then some properties here. So tenant array field name will be tenants. Tenant collection slug will be tenants, which needs to match the slug inside of our tenants collection.
Tenants array tenant field name will be tenant. Array field access for now will be read set to true. And we're just going to copy, create and update set to true. For now, later on, this won't be true. And we can copy these and change them to tenant field access and keep them all to true as well.
Just for now, obviously, this is just so we can test this easily. And now let's go ahead and spread this field inside of our fields array for the user here. So I'm just going to open a new field and we just spread the field above and add some additional things like admin. Let's copy whatever we might have inside of the default tenant array field?admin, which might be an empty object, and position sidebar. So now we have attached the tenant to be controlled by the user collection, but the tenant is also in close relation with products because of the collection here.
My ball is the config right here. So I think that still not much should change here, but we are gonna go ahead and start improving this now. So we have added the tenant field, and now we have to create the tenant on user registration. So let's go ahead inside of source, inside of modules, out, server procedures, and let's find register. And now, once we create the user, we also have to create the tenant and connect those two.
So let's go ahead and do the following. Let me just see. So we first check if this exists. So only here should we do that. Const tenant will be await context.database.create.
Collection will be tenants. Again, if you have TypeScript errors here, meaning that tenants are not an option here, you most likely have to manually run generate types, so you have updated types. Now let's add some data here. The name will simply be input username. So whatever user entered in the registration form for the username, and the slug will be the same because the username has to be unique.
The name is something that user will be able to change later, right? So they can rename it to Antonio's store instead of just Antonio, Right? And let's add Stripe account ID for now, mock. Let's just do that or test. And I seem to be having an error here.
Let me quickly go inside of my tenants collection to see that I didn't accidentally do something incorrectly. This is required. This is not required. Okay. But let me see.
Email is declared here in the tenants. That is odd. I'm going to check if that is correct or not. So I don't know why it's expecting email here. My version did not need email, but let's see.
What if I do input.email? Because that has to be unique as well. It could be just an updated package, right? Because I'm definitely using a newer version than what I developed with initially. So maybe they've changed it so that you need an email here as well, which would make sense if it is going to be connected to the user.
Because of this, Let's go inside of the tenants collection and remove out, right, the outfield snuck in here because we've copied from users. So just do a quick check, make sure we didn't do that mistake somewhere else because that would be bad. Looks like we didn't. Okay, so now you can see that it doesn't need the email. If yours still needs the email, run this.
So make sure your tenants collection doesn't have the outfield. Excellent. Now that we have this, let's go ahead and connect this tenant to the new user right here. So I'm just going to go ahead and add tenants. And it has to be an array because this plugin actually enables individual users to own multiple stores.
That's not going to be the case for our product. We are going to limit one store per tenant just for simplicity's sake. But later on, you're going to have no problems configuring this further and allowing multiple stores. And that's actually it. This is the entire logic you need to create multi-tenant architecture.
So this is what we are going to do now. We're going to go inside of our seed script right here. And what we're going to do is this is where we're going to create the super admin. The reason I want to do it from the seed script is because even if you were to run database reset, you would get prompted to create an account on the Payload admin page, but from their registration, I'm not sure how to exactly create the tenant, right? So instead, what we're gonna do is the following.
Not, sorry, not how to create a tenant. The admin doesn't need a tenant, but I'm not sure how to give the proper role of super admin. That's what I don't understand. So instead, I'm going to do that in the seed script. So create admin user await payload create collection users data email admin demo.com password demo roles super admin username admin.
That's it. That's all we need. So let's go ahead and let's reset our database. I'm going to shut down the app and do bun run database reset. We are going to get prompted asking if we want to delete all data.
You can safely press yes when it appears. There we go. And then it's doing database run seed, which means adding the categories and this time also adding the admin. So now go ahead and do bun run dev. And let's go ahead inside of localhost 3000 here.
If you were previously logged in, you will now be logged out because your cookie is invalid. Let's go ahead and click Login here. And you should now be able to go inside of admin at demo.com and demo password. There we go. So you have a role of super admin and if you go to your dashboard, you are going to see something new.
At least you should see something new. It's not visible yet, but if you go inside of your account, so click here, you will see the role of super admin right here. And you can also see that it offers you to connect your account to a tenant, right? But that's not what I wanted to show you. Go ahead and log out.
You can use this button here. Behind the next button, you can click the log out button. And go to the normal login page. Go to the register page here. And let's create Antonio, and Antonio at demo.com and demo, and click create an account.
Now, what will be interesting is that you can already see I have a tenants array here and a connected tenant. So now I should be able to create products here which are only connected to my tenant, which I can see right here. Right now, I can also remove myself from a tenant. All of these things will be protected in a better way. This is just for an easier demonstration.
So let's go ahead and create a product here. Looks like I'm having an error here. It could be because I have set something up incorrectly. So let me try going back to admin, the products, create new product. Maybe I set up something incorrectly.
I'm going to go ahead and recheck. So I believe I found the exact issue that we got here. Multi-tenant example, cannot destructure property config, and then this what seems to be a bundled code and looks pretty much the same as ours. And I think what it might be, as you can see here in a few comments saying that they've deleted node modules and the lock file and reinstalled the packages. So I guess this could be due to the fact that maybe when I have this little caret here, they have a slightly different log file than this one, which is the exact version.
So here is what I'm going to do. I'm going to delete my node modules. That's the first thing I'm going to do. So rm rf node modules and shut down my app, of course. So Nodge modules deleted.
And I'm then going to also delete my .next folder in case there is any cache. I'm then going to rerun bun install. So I didn't delete the log file. I don't think there's the need to delete the log file. And now I will do bun run database reset again, right?
I just want to have a complete fresh database here. Let's confirm. And then run the seed script. And it looks like we have some error here. So maybe this is what's happening.
This is probably, I think that this is either MongoDB or maybe, let me just see what this is. Write conflict. Let me just research a bit. Perhaps we need to modify our seed script. So these are some errors here.
Let me see If I just do a reload window, do these errors go away? They seem to go away after I reload. So what I'm going to do is I'm also going to create admin tenant here. So await, so const admin tenant will be await payload create collection tenants name admin whoops I have to put this into data name admin username admin and my apologies slug is admin stripe account id well it can also be admin, it really doesn't matter. And then in here I'm gonna do tenants ID tenant admin tenant dot ID Let me just check if this is correctly created.
Admin tenant is a tenant here. And in here it says that tenant is missing. I have access to the ID here, but it says that this is not enough. Let me see what am I missing here? I'm sorry, it's tenant.
There we go. I should have just looked at my ALF procedure. So basically what I've done now is ensure that even the admin has a tenant just in case. And now I'm just going to try and do this again and maybe do it manually just to make sure maybe it's something with the concurrency of the script let's try manual reset of the database first let's confirm and still the same error So I'm going to go ahead and try and research what this error is. Okay, so I think I figured out what is the issue.
I thought it was something with the categories, so I have commented them out, but it's not. So everything is completely fine with our code here. But I think something's wrong with this script. I think database run seed runs before this has finished. I think that's an issue.
So I'm not going to be using this script anymore. Instead, I will be running them independently, database fresh, like this. And then after I confirm that this drops the database by confirming and then do bond run database seed. Let's see. There we go, seeding completed.
So I think it was that script. Probably not the correct way I should create that script. I'm going to explore later if there's a way to improve it, but as of now I think it's okay like this. Let's give it another shot now. So I'm going to go to localhost 3000 here.
And I'm going to go ahead and create a new account Antonio, Antonio at demo.com and demo. I automatically have a new tenant now and if we still have the same error I'm going to try again but this time I will delete the lock file. So I can access the products. And there we go. I can now create a new product.
So look like it was just a fluke. After you add the multi-tenant, go ahead and do bun run. My apologies. Rmrf node modules and rmrf.next. And then do bun install, and then bun run database fresh, and then bun run database seed.
Right, So that is a solution for our error. At least in my case, I just had to reinstall all packages. So I think the lock file is now assigned. So I'm going to call this Antonio's product. I'm going to copy the same thing in here, give it a price of 50, select a category and no need for a tag at the moment.
And let's click save. So now in my products here, you can see I have Antonio's product. And now I'm going to go ahead and log out here. And I'm going to go here just to see if it loads in business and money here. Because I think that's the category I put in.
There we go, Antonio's product. Now I'm going to go ahead and create john demo.com. My apologies john john demo.com and demo for the password. And I'm going to go to the dashboard. And you can see that now in my products, I cannot see Antonio's product, I can only see the product that belongs to my tenant.
So this is John's product now, 100. And let's set a category to business and money as well. And let's click save. You can see that I can only see John's product when I'm logged in as John, right? But if I go to AntonioDemo.com, in my products, I can only see Antonio's product.
But if I go ahead and use my admin, right? If I use my admin, demo.com, I should be able to see all of them. Admin, demo.com, and demo. There we go. You can see that I can either choose a tenant I want to look at, or I can just look at all of them.
So if I click on products, I can see both John's product and Antonio's product. Or I can pretend to be Antonio and then I can only see there. Or I can pretend to be Antonio and then I can only see there. Or I can pretend to be John and then I can only see John's. So that is exactly what I wanted for us to achieve with this multi-tenant structure here.
And while we are here, let's also enable the All button because All doesn't do much at the moment. So in order to fill this page, I'm just going to go and copy from my category page. I can copy the entire thing. And now I will go inside of home page here and I will replace it entirely like this. The only thing that's going to change is the fact that I don't have the params.
I still have search params. I just don't have the params, which means that category in this case does not exist. And that should be completely okay, right? We don't need to have a category. If we don't pass a category to get many, we are just not going to query by any specific category in the where.
And there we go. You can see that now in all, you can see all products. There we go. So That was what I wanted for us to do. You can also go inside of contents and increase the default limit to eight, right?
Just so you can, just so it makes more sense. And if you are logged in as an admin, you can also go to admin demo.com demo. You should be able, you know, to modify any product. So for example, go inside of John's product and change from business and money to, I don't know, let me go to something, to web development or software development. Let's click save.
And now just to prove that our all filter works. So on all, they are both loaded, but in business and money, only one is loaded until I go to software development here, web development, and click on software development here. Not the best UX, but yeah, okay. At least we can go here. There we go.
And we can now see John's product, right? Excellent. So this is a great start. I think we did everything we wanted. We create tenant on registration and we connect products collection to a tenant.
So each product now belongs to a tenant which is essentially a shop. So just to recap, you will most likely get the same error as I did. So remove your node modules and reinstall your packages. Looks like our script for combining fresh and seed doesn't work as expected. So if you get the same error that I did, which was this one, Let me try and find it.
This is the error. Transient transaction error. Basically it says that some IDs are already in use, which is, you know, a write conflict is almost a telltale that your old data was not deleted. So obviously you first have to run this, wait a couple of seconds, and then go ahead and run this right here. Great, amazing, Amazing job.
That's it for this chapter. So we're going to go ahead now and create 15 multi-tenancy branch. Git checkout be 15 multi-tenancy. There we go. Git add, git commit 15 multi-tenancy and git push uorigin 15 multi-tenancy.
Once you have confirmed you are on your new branch, Go inside of your GitHub, open a pull request here, and create a pull request. Here's a summary. We added the multi-tenant support, enabling tenant management, and enhanced role assignments. We also added the new product list view to our home page. So it detected that, as well as the increased default product limit.
We updated the registration process to automatically associate new users with a tenant, and we fixed our invalid drop-down filtering menu bug. As always, walk through file by file, you can see how we removed the database reset script. It wasn't unused, it was just not working correctly. And it also detected the multi-tenancy support by adding a dependency and updating the config, the collections, the type, everything we need, along with the seed script. In here, we have a sequence diagram, which talks about how we create a tenant using the username as name and slug.
And we return the tenant ID, which we then use to create the new user. And that's how registration works for us. In here, it gives some good comments. We don't need to do this because we already do it using Zod. But we could implement logic error handling if tenant creation fails.
That could be the potential fallback, but we don't have to worry about this. We already thought of this. The tenant can only be, the username can only be something that will be used as a domain. So I'm gonna go ahead and merge this pull request. There we go.
Let me just confirm that I have this branch here. I do. Perfect. And now We're gonna go ahead and do git checkout and make sure you are on your main or master branch and git pull origin main or master and git status. There we go.
And confirm with your graph right here. Perfect. So we can see that we added the new multi-tenant plugin here. We can see that we changed the default limit. We can see that we added the tenant.
We can see that we added the tenant field to user along with the roles. We can see the new creation of tenant in the registration procedure. We can see that we removed the faulty dropdown position hook. And we can also see the modified seed script. This is the config for multi-tenant and the modified seed script right here.
And somewhere in here, it was the package.json, but I can't see it now. There we go. So we removed the seed script. Perfect, confirmed that you are on your master branch seeing that. And that's it.
We pushed the GitHub. Amazing job, and see you in the next chapter.