#115 Multitenancy, Teams and Roles without a gem

03/03/2023
moneygun Mulitenancy saas software architecture
  • Multitenancy examples
  • static pages controller
  • install devise
  • scaffold tenants
  • generate members resource
  • tenant creator becomes admin member, member authorization
  • install devise invitable
  • diplay tenant members
  • invite users to be members of tenant
  • member authorization
  • Final demo and thoughts

Ruby on Rails #115 Multitenancy, Teams and Roles without a gem

Want to learn more on the topic?
Here's a DISCOUNT for my complete course Ruby on Rails: Learn to Build a Multitenancy SaaS app: https://yshmarov.gumroad.com/l/ror6saas/presale33

In this episode we will:

  • let users register
  • let users create tenants/accounts/organizations/workspaces
  • user who created tenant automatically becomes admin
  • invite users to become members of the app
  • invited users are not admins
  • allow only members to access parts of the app scoped to the tenant

Setting tenant based on routes is the best, because you can have multiple tenants open in different tabs (unlike adding current_tenant_id current_user).
Second best option is subdomain multitenancy, but that is harder to pull off and maintain.
Row-based multitenancy is the most popular choice, and it is used in Rails SaaS starterkits like Bullettrain and Jumpstartpro.

Episode source code: https://github.com/corsego/116-multitenancy-routes-rows/commits/main
Database architecture: https://dbdiagram.io/d/63fbe3ed296d97641d83db94

0:00 Multitenancy examples
4:35 static pages controller
6:28 install devise
8:48 scaffold tenants
10:31 generate members resource
12:34 tenant creator becomes admin member, member authorization
16:12 install devise invitable
17:31 diplay tenant members
20:20 invite users to be members of tenant
26:42 member authorization
28:50 Final demo and thoughts

Transcript
What is multi-tenancy? Wikipedia says that it is a group of users who has common access to an instance of an application. Now let's have a look at a few examples. For example, we've got Jello. I have created my user account, and inside my user account I have created a workspace. And inside this workspace, I've got different information that is scoped to this specific workspace that isn't present in any other of my multiple workspaces. For example, I've got these columns and cards inside these columns that are not shared with any other workspace. So everything inside this workspace is inside this kind of tenant. And I have, uh, my membership in this tenant. And you see, I'm the admin of this board. So you see this member is an admin of the board. So I'm a member in this talent, and I can invite other users to become members of this talent. So I would need to, to add somebody's email address, add, uh, that role that I would want to give this user. And I would send a user invitation to, uh, a person to join this, uh, workspace. And again, I can have, uh, different access rights in different workspaces. Another good example would be slack. Here are multiple workspaces, and there are multiple members in each of these, uh, workspaces. And uh, you can be either an admin, you can be just a user inside one of these workspaces, but like all the channels, all the messages and so on, everything is scoped and contain inside this workspace. So no information has been leaked from one workspace to another. And, uh, there is, for example, Basecamp that, uh, also has, uh, different organizations. You can create, uh, one or multiple, uh, teams and invite members to the teams and everything inside all projects, everything is scoped to a team. And if you have a, a look at the UL inside Basecamp, you see the idea of the talent. So this is like your organization ID that you're looking at at the moment. And everything else is scoped under this idea of the organization that you are looking at. Um, similar with the cello here is I guess the idea of the organization and, uh, yeah, I guess the name of the organization. So if I would go to another tenant, then uh, the idea here would change. Now how do we build this kind of multitenancy in bound rails? So here's the basic, uh, kind of database architecture where we have a user here, again, in Trello or in Slack, I have the same user instance, the same user with one email, and there are multiple workspaces or tenants. And I can be a member of one or many, uh, workspaces or tenants. And, uh, member belongs to the user, a tenant and has tr So you see, I can have different trolls in different organizations. And then there is other types of data that are scoped under the tenant. So here for example, we have, uh, columns and cards and possibly comments. All of them have, uh, tenant id. And that way we can see all the tasks or projects or whatever that belong to the specific tenant. So everything that should be scoped to a tenant should have the tenant id. And this is called row based multitenancy. Other types are database multitenancy or schema based multitenancy. That gym apartment, uh, as far as I remember it, uh, offers a schema or database, uh, multitenancy, but it is, uh, not, uh, a more sustainable approach. And there are a lot of gems that, uh, offer you, uh, road based multitenancy, like exist tenant and active multitenant. Now, uh, bullet train, uh, this, uh, uh, software as a service, uh, boilerplate for ribbon rails offers teams and invitations as one of their core features. And they use a similar type of multi-tenancy as in Basecamp. So again, everything has a tenant id and they use, uh, route to set the current talent. And there is Jumpstart Pro and other stars, uh, multitenancy boiler plate for Ruben Trails that has multitenancy and accounts as one of their core features that they pay. Uh, they, you expect you to pay a premium price, uh, for, so yeah, let's try to build this kind of functionality. Now, here we are not going to use any gems, so we're not going to use any of these gems by the, we're talking about the gems. So apartment, I don't like it. Existent is very good. Active record, multi 10 is very good. Mil used the good five years ago, but now it's outdated. And this games gems. I haven't tried and Jumpstart Pro actually use this X as talent. Okay? So let's try to build Hamilton architecture where we have, uh, uh, different tenants or workspaces. User creates a workspace, he becomes admin by default, and user can invite others to become a member of this workspace. And a user who isn't part of this workspace can't see anything inside this workspace. So let's start by a new Ribbon rails application. I will, um, yeah, I have actually just, uh, created a new Rails app and, uh, let's, uh, start the rail server and have, uh, a look. Okay? So yeah, there is nothing. Let me create a few pages. So let's create a landing page that will be accessible by not logged in users and dashboard that can be accessed by logged in users. So let's say rails generate controller, uh, static pages, landing page and dashboard. Okay, let's open our code and, uh, let's go to our roots and say, uh, route will be to static pages landing page, and we will get the dashboard to static pages. Dashboard. Let's, uh, see if it works. Rail server, Uh, missing controller key. Okay, maybe I missed something here. Got, uh, okay, yeah, controller, key rail server. And here we are on the landing page. We can go to slash dashboard and we can see it. Okay, let's, uh, add the, the links. So application dot html, er BI will add the, uh, link to, uh, home, and it'll be a root path. Oh my god, too much help from the IDE and link to dashboard. And it'll be dashboard path. And let's see, we have two links. Okay, uh, let's save our changes. So I will say get status gi, add all GI Comet, uh, a static pages controller. Okay, now let's add device. So let's say rails. Uh, no actually bundle add device, uh, rails generate device install. We'll need to add these lines to our application, our B four flash messages, and we will need to add, uh, this to be able to send emails in our development environment. And, uh, yeah, let's now, uh, say rails generate device user to create our user model and migrations rails DB migrate. And let's require a user to be able to, to, to, let's require user beside in to be able to use our app. So before action authenticate user. And we are going to skip this before action for our landing page. So I will say skip before action authenticate user only Landing page. Let's see if it works. I will start the server. And let's see. So to access the dashboard, I need to be logged in, but for homepage, I don't need to be logged in. Let's, uh, try creating an account. Okay, let's now add the links to like send Inside Out. Now, I'm not going to write them down manually, I'll just copy them from somewhere. I, uh, remember. So going back to application HTML. And in our navigation part, I will, uh, add, so if the user send in, we can see his email and link to log out and link to the dashboard. And if the user is not sent in, uh, yeah, in all cases we'll see the link to the homepage, but we'll see also login and register. Let's go back and refresh. Okay? Home dashboard, my email and log out. And if I'm not logged in home, log in and register. Okay, looks good. Let's save our changes, get status, get add the all gi com main, uh, installed device. Okay, looks good. Now, let's go back. And here we have, so we've got our users that were provided by device, and let's create our tenants. So let's say rails generate scaffold, tenant, and then we'll have a name. Okay, let's, uh, start the server and have a look at our tenants. Yeah, we should actually run the migration rails to be migrate. And let's go to tenant RB and say it validates name presence, true. And let's add the link to tenants inside our application, followed in user. So let's make it tenants both. Uh, surely you can name it no tenants, but like workspaces or something like that in your application. So let's see. I will, uh, log in and have the tenants path. Okay, uh, Maybe I made a typo, uh, validate. Yeah, I've got s okay, let's create a talent. And I have created a talent. Looks good, but at the moment, the user and talent are not related at all. So we need to, uh, know that a user is a member of the talent. And for this, we're going to add this member association. So this is like the team access. Uh, a user will have many tenants through member and tenant will have users through member. So it'll save our changes and continue get status. Here we have just added the scaffold of tenants. Get add all, get commit, main scuff tenants, and let's add our, uh, member model. So let's say rail generate, uh, resource. And we will have member, we will have, uh, user references and tenant references. And importantly, they'll, they'll also have a roles table. So, uh, they're going to know that this user is an admin in this organization, but he's not an admin in the other organization. So we will say roles, JSOB. Now let's go to our migration and have a look at it, the B migrate here, and we'll say no, false and default will be an empty hush. Okay? Uh, rails, db, migrate, and let's add the business associations now. So going to our model VC member, member belongs to user internet and user will, uh, let's say has many members and has many tenants, uh, through members. And we'll do something very similar for our tenants. Tenant, uh, has many members and has many users through members. Okay? So, uh, yeah, let's save our changes, get status and have created the members controller. Uh, they've added roots, but uh, no views for the members for the moment. So I've generated no scaffold, but, uh, uh, resource. Let's have a look at the members controller. Yeah, it is just an empty blend controller. And in the roots we have just resources members, okay? So let's say Git add all gi, commit, message, uh, generate, uh, members resource. And now we need to make it so that when a user creates a tenant, he will become a member of the tenant and the user should be able to see only tenants that he's a member of. So let's go to our tenants controller, and here we will say, we will show not all the tenants, but the current user tenants, okay? And you see, I'm not a member of any tenant. And to make the current user member of the tenant that he has just created, we'll go here. So if the tenant is saved, we will create member at talent members, great. And we will say, uh, uh, user will be current user, and we can also assign a role like roles admin. True. Okay, let's see if it works. I will create, uh, at talent, uh, with member, and we have created a talent. And going back to tenants, you see, I can see this talent. So I'm most likely a member of this talent. Let's go to the console and check member dot account member dot first. And you see we have added this, uh, membership. So now I am the, a member of this tenant. And let's see, can I access another tenant? Let's go to tenant slash one. You see, I can still access this talent, uh, show page, but I don't see it in the index page. So, so I shouldn't have access to this, uh, tenant at all because I'm not a member of the tenant. How can they do it? Let's, uh, go, um, to tenant controller and let's say, uh, return redirect to, uh, route Path alert. You are not a member. Uh, and we'll do it, uh, if, uh, the talent. Uh, so unless, uh, talent dot, uh, users dot include current user. Let's see if it works. So yeah, you see, I'm not a member. I was directed to the main page. I'll go to the time that I'm a member of and I can see it. And we will add this, uh, as a before action to all the places where we need a current tenant. So I will say, uh, before action, uh, authorize member And I will apply it to the same, uh, actions and let's, uh, add this private action, private method and move it there. So authorize member, and let's see, can I go to tenants one slash edit? No, I cannot, but I can go to tenants two slash edit because I'm a member of this tenant. Okay, looks good. Let's, uh, save our changes. So, uh, yeah, we've done only changes in the tenants controller. We made it so that when we create a tenant, the create becomes an admin of this tenant. And we've also made it so that only, uh, tenant members can view tenant pages. So get, add all, get permit, um, tenant, uh, creator becomes, uh, admin member and authorization member of the authorization. Okay? Uh, what is next? Now, we want to be able to invite other users to become members of our application. And for this, we're going to use device inviable because if a user, if we, let's say have this kind of form where we inputter user's email, which to be able to actually create a user in our application and send him an invitation link to become a member of our, uh, application and also make him a member of this tenant. So we are going to install the GM device invite, And let's see how it is done. So I will say bundle add device inviable install, add it to the user model, run the migrations, and that's about it. Let's see, use of rb. We've got device, inviable and uh, dev device, rb, we have added, uh, the inviable, uh, here. And in, uh, our user model, we have added the, the invitation, uh, attributes. Okay, let's, uh, save this and move forward. So get status gi add all GI comment, main, uh, message install, device invite. And now let's actually create a form where we can input a user's email to be able to make him a member. But first we would need to have like a list of all members of it inside the tenant. So let's, let's say like we will open a tenant, okay, we have this error. I think I just need to restore the server, okay? Yeah. And let's, uh, within a tenant have a link to the members. So tenant show. And here I will have a link to members. And the members should be scoped under the tenant. So it would be something like tenant members path. Let's go to our roots and input the members under the resources tenants, Okay? Uh, let's go to our show view. And here will have add talent. Let's see if this works. Yeah, we have a link to members. And here you see we have tenant slash two slash members. And inside members controller, we don't have index action. So let's edit the index. And here we will have add members equals current tenant dot members. And then to set the current tenant somehow. So let's say private dev, uh, set current tenant, and we will say at current tenant equals tenant defined by, uh, PERS tenant id. Because you see we have this tenant ID in the, uh, in the route. So yeah, and, and we will add the before action set, current tenant, and this should kind of work a refresh, okay? No index, uh, view. So let's go to Vs. Uh, members. Okay, I've was created a folder. Let's make the index H, tml, eer B. And we have an empty view, let's say, uh, at members each do member, and we will display the email. So let's say at member user email and the roles at member shows, okay, yeah, if that playing the members of the standard, that's good. Let's actually save this change. So get staples, Get add, all, get, commit, message, uh, this lay, uh, talent members, okay? And now it's actually do this invitation team. So let's add the form to invite a member. Let's maybe list all members, H one, uh, members, and separate the thing for inviting members. So invite members and I will have a form with URL. We'll input A URL here, do form. And here we will have equals form text field. We're going to input an email that's add a placeholder, and we'll have a submit button, form dot submit, uh, invite. And uh, what will the URL be? Let's, uh, construc the URL. Uh, so for example, it can be, uh, resources members do, uh, post invite. And this is going to be inside collection, collection door post invite. Okay, let's see this rule that we have just created, let's say rails rules, grab invite, uh, yeah, rails route. So Invite tenant members both. So let's go back here and it'll be URL, invite, tenant members path. Let's, uh, see if it works for us. I will refresh. And here we have the phone for inviting members. Let's see the URL. We have tenant slash two members invite. Let's try, click on this invite and see what happens. So nothing happened because we don't have this action implemented, and we can see an error here. Action invite is not defined. So let's go to our members controller and say, dev invite. And here we can try getting the pars. Or let's say we'll just put the binding binding B and let's try clicking the invite. Let's put something in the email and they're in the bindings. Let's say perms. We have tenant id, we have, uh, email. So let's say we can work with this. So we will say email equals brands email. And we are going to, uh, find a user by this email user equals user dot find by email. And if we don't find the user here, they're going to use device inviable. So, uh, they're going to send invitation invite. Let's see, uh, yeah, they're going to have this line. So, uh, user invite by the email from the pre and current user. And then we are going to say, user, do members find or create buy and we'll say talent. There'll be, uh, current talent. And we'll also set roles as, for example, admin, false and, uh, editor. True. Okay. Uh, let's see if this works. I will drop the binding, continue. And let's see. So, uh, I will go here. I will, uh, try to input, uh, some kind of, uh, email click invite. And did anything happen? Uh, yeah, we did send some kind of email, but, uh, did we make the user a member? Let's refresh. Yeah, the user has become a member. Okay, I need to refresh because I didn't make a redirect here. So I will say, uh, redirect tool, uh, what is the path? Turn on member path at current tenant with a notice, uh, uh, member invited, or let's actually put the email. So let's say, uh, email invited. Okay, let's try again. I will input some kind of, uh, other email address. And here we have email invited and he has been added to this list. Okay, looks, uh, quite good. What if we have a look at some outliers? What if we, we submit an empty form, we will get some kind of error. Most definitely record, not saved. You can not call, great. Unless the parent is saved. So let's actually add some validations. Let's say, uh, return redirect to, uh, tenant member's path, uh, with an alert, uh, no email provided. Uh, if email is blank. And we'll make another return, uh, here if, uh, the user is not valid. So user dot, uh, valid, unless, unless the user is valid. So we'll put these guards to check if we have submitted an email and if the user with this, uh, email text is, uh, valid. Okay, let's, uh, let's see. I will try and put in an empty email. You see, no email provided something like this. And uh, yeah, it would have to say, yeah, email invalid. So email invalid. Let's try once again. Okay. And this email is involved, so our guard closes well correctly, and yeah, that's it. We've managed to invite users to become members of this tenant. Let's, uh, save our changes. Let's say get status, get, uh, all, get the comment message, uh, uh, invite, uh, users to be members of talent. Okay, and, uh, now let's check some validations. Can I go to tenant slash one slash members? And yes, you see I can, but I'm not a member of the first tenant, so we would need to put a guard for this. Uh, in our members control, you see, we have this authorized member inside tenants. We need something similar inside our members controller, but they would need this kind of, uh, set tenant and authorized member for all the controllers that belong to a tenant. So everything that's scoped, uh, to your organization here, like tasks or projects or in Trello, like, uh, all their columns, cards, comments, inside cards, everything would have to have, uh, authorized number and set current tenant. So we can, uh, not just duplicate this code. Instead we will create another controller. Let's make it authorized controller rb, I will say clause authorized controller that inherits from application controller. And here I will put the, the line for set current tenant I'll removed from the members. I'll move these lines also to the authorized controller. I will, uh, also add the before action authorized member. So we will first set the tenant and then we'll authorize the member. Yeah, let's also change the order here so that it is, uh, hierarchical and importantly inside our members' controller. Now we are going to inherit not from our application controller, but from authorized controller. So, uh, all this code is going to automatically included in members' controller. Let's see if it works. I will go back to the application. I will refresh this page and define method users. Let's see, uh, yeah, because I should say current talent And yeah, I'm not a member of the talent, so it kind of works. Now we've got this kind of additional authorization so that only members of tenant can view like all the pages inside this tenant. And let's see how our application works. So let me create a new, uh, account. I've registered as a user, I can go to the list of tenants. I'm not a member of any tenants. I can not visit any tenant because I'm not a member of any. I can create a new tenant and, uh, yeah, I've created this tenant. I can go to the list of members and, uh, I can invite another user to be a member. If I invite an user that already exists in the application, he'll be just added as a member of the attend as an editor, not as an admin. And, uh, if I, uh, invite somebody new, so I click invite, they're actually going to send an invitation email for a user to be notified that we have invited him to our application actually as an additional start that you can do on your own after inviting the user to become a member, you could also send an email that he has been added to this workspace space. Yeah, something like this, it's something you can do on your own. And, uh, yeah, that's, uh, basically it. So, uh, uh, if I log in as this user, I will, uh, be able to view the tenant. Let me try accepting the invitation. Uh, yeah, I'm already signed in. Let me Log out. I will, uh, accept the invitation, set the passport, and go to tenants. And you see, I have been invited to this tenant. So the same as Slack as, uh, Trello. Uh, you have this kind of tenancy architecture built, and uh, yeah, here is a nice bullet plate based on which you can add any kinds of, uh, structures that belong to a tenant or like a workspace, but not to a user. So this is how you build in teams into a rub rails application from the very beginning. And if you want to add any additional tables, again, tasks or projects, everything with this kind of route based setting of the tenant would have to be scoped under resources, tenants. And I think it's very comfortable because, uh, this way if you have, uh, the current tenant set in the route as base Camp does, you can have, uh, a few different tenants open in different tabs. So, uh, other approaches like, uh, a popular approach for be to set, uh, tenant ID in the current user and user would have to switch tenants. But, uh, if you set the tenant in the roots, then you can have like ten three open. You could, uh, create a new tenant, uh, And have ten four open. So I have ten four here, 10 through here, and it works in different browser tabs, and you don't have to like have a separate button to switch tenants. So, yeah, that's about it. Thanks for being with me and thanks for creating this, uh, um, multitenancy application together with me. See you in the next one.
2
Join the conversation
Sign in to access PRO lessons, access private repos, leave comments, create watch lists.
We collect your email address, name and username to create your account. We do not share your email address with anyone else.