Multitenancy in Jumpstart Pro: How do you envision it working?
A lot of you have brought up the idea of multi-tenancy in Jumpstart Pro and I want to look into adding this, but I'd like to hear how you guys intend on using it so I can build a version of it that's helpful.
How do you want tenants to be looked up? By domain, subdomain, or something else (like the first part of the route /:tenant/projects)?
Jumpstart Pro has teams and team billing, so would each team be the tenant? This would mean that billing would happen separately for each tenant. You could create multiple tenants and each would have their own subscription.
We'd probably do this by including the tenant ID on each model and automatically scoping it out like the `acts_as_tenant` gem does. This safely handles any performance issues that multiple postgres schemas cause. Thoughts?
What are you trying to accomplish with multitenancy?
How do you want tenants to be looked up? By domain, subdomain, or something else (like the first part of the route /:tenant/projects)?
Jumpstart Pro has teams and team billing, so would each team be the tenant? This would mean that billing would happen separately for each tenant. You could create multiple tenants and each would have their own subscription.
We'd probably do this by including the tenant ID on each model and automatically scoping it out like the `acts_as_tenant` gem does. This safely handles any performance issues that multiple postgres schemas cause. Thoughts?
What are you trying to accomplish with multitenancy?
We have multitenancy in an existing app to keep certain resources private to certain users. Everything is in a single Postgres schema and we use a `School` model as the tenant. We use a method described by Ryan Bates way back in Railscasts - setting the current school ID based on the currently signed in user in `application_controller.rb`
```
class ApplicationController < ActionController::Base
```
class ApplicationController < ActionController::Base
around_filter :scope_current_school
def current_school
current_user.school
end
helper_method :current_school
def scope_current_school
School.current_id = current_school.id if signed_in?
yield
ensure
School.current_id = nil
end
...
```
then using default scope in all models that are private to the school:
```
class Lesson < ActiveRecord::Base
```
then using default scope in all models that are private to the school:
```
class Lesson < ActiveRecord::Base
...
default_scope { where(school_id: School.current_id) }
...
```
It's worked really well for us for 5 years. The upside is it's robust in separation while still keeping everything in one PG schema. The default scopes ensure nobody sees any resources from a different school. The downside is it can make working in the Rails console a bit trickier.
```
It's worked really well for us for 5 years. The upside is it's robust in separation while still keeping everything in one PG schema. The default scopes ensure nobody sees any resources from a different school. The downside is it can make working in the Rails console a bit trickier.
We have multitenancy in an existing app to keep certain resources private to certain users. Everything is in a single Postgres schema and we use a `School` model as the tenant. We use a method described by Ryan Bates way back in Railscasts - setting the current school ID based on the currently signed in user in `application_controller.rb`
class ApplicationController < ActionController::Base around_filter :scope_current_school def current_school current_user.school end
def scope_current_school School.current_id = current_school.id if signed_in? yield ensure School.current_id = nil end ...
then using default scope in all models that are private to the school:
class Lesson < ActiveRecord::Base ... default_scope { where(school_id: School.current_id) } ...
It's worked really well for us for 5 years. The upside is it's robust in separation while still keeping everything in one PG schema. The default scopes ensure nobody sees any resources from a different school. The downside is it can make working in the Rails console a bit trickier.
I suppose different projects have different needs.
I'd imaging Marketplace Apps and Private/Team SaaS Apps (what i'm building) would generally be in the format /:tenant_slug/projects
It's simple and format will be good for Google/SEO. Also people can link directly to pages (unlink how it currently works with logged in cookies required).
I'd imaging Marketplace Apps and Private/Team SaaS Apps (what i'm building) would generally be in the format /:tenant_slug/projects
It's simple and format will be good for Google/SEO. Also people can link directly to pages (unlink how it currently works with logged in cookies required).
Tenant Branded Websites
I can see why others might need subdomains or domains for projects like website builders, online shops etc. where branding needs to be unique and content not in competition with others on the same domain.
I have built some projects like this is the past. Subdomains were simple enough. custom domains got a bit complicated for me.
I have built some projects like this is the past. Subdomains were simple enough. custom domains got a bit complicated for me.
For me it would be cool, but I’m not sure if/when I’m going to need it anytime soon. That being said, as a template I would think that it should support both sub domain and nested route. It’s always easier to turn routes off than to set them up.
Right now I have a spree project that has a subdomain setup for multi tenancy but needs switched to use nested routes. Would have been nice to have it both ways by default.
Right now I have a spree project that has a subdomain setup for multi tenancy but needs switched to use nested routes. Would have been nice to have it both ways by default.
My use case would be a combination of
Will Barker
and what
John Chambers
mentioned (Tenant branded website - I'd like to use subdomain, and if possible allow them to use a custom domain mapped to it).
Two other things:
(1) For me, I'd like to be able to control pricing based on a plan that charges by users tied to that tenant (i.e. $50/mth for up to 100 users, $150/mth if they have 101-999, etc.). Even better if that amount can be overwritten (i.e. manual discounting). I'm working on a B2B SaaS app.
(2) For my app, I'd prefer it if users joining a tenant (a non-admin user), join without a team/tenant being created for them automatically. I'd like two separate paths for someone creating an account with the intention of creating (and administering) a tenant versus a user who is just trying to join an existing tenant. Perhaps the person just trying to join as an existing tenant has to join via an email invite, and others join via sign-up form on the site.
Two other things:
(1) For me, I'd like to be able to control pricing based on a plan that charges by users tied to that tenant (i.e. $50/mth for up to 100 users, $150/mth if they have 101-999, etc.). Even better if that amount can be overwritten (i.e. manual discounting). I'm working on a B2B SaaS app.
(2) For my app, I'd prefer it if users joining a tenant (a non-admin user), join without a team/tenant being created for them automatically. I'd like two separate paths for someone creating an account with the intention of creating (and administering) a tenant versus a user who is just trying to join an existing tenant. Perhaps the person just trying to join as an existing tenant has to join via an email invite, and others join via sign-up form on the site.
I don't think teams should be tenants. Some SaaS products bill by the team/workspace/group, so it's nice to have that support, but I feel like there should be an actual Account or Subscription model with the subdomain/tenant. I don't think I currently like the default of coupling billing so tightly with teams. Teams should be left up to the developer to implement, in my opinion.
A subdomain approach works best when there's a portal/information needs to be displayed unique to the tenant before a user is logged in. If your app doesn't need to display anything unique on the subdomain, or even the login page (such as a custom logo) then the app.domain.com/:tenant approach works fine. I think Jumpstart should aim to support both.
A subdomain approach works best when there's a portal/information needs to be displayed unique to the tenant before a user is logged in. If your app doesn't need to display anything unique on the subdomain, or even the login page (such as a custom logo) then the app.domain.com/:tenant approach works fine. I think Jumpstart should aim to support both.
I agree with Sean, teams shouldn't be tenants. I'm looking to add multitenancy using the acts_as_tenant gem. I'd be adding an account model which would be the tenant. I would probably look to have an 'accepts_nested_attributes_for' relationship between Account and User. I would see that there'd be a has_many relationship with Account and teams as well.
I think another nuance with this all would be shifting the billing away from the team and more to the account. I'd actually like to have a basecamp-type subdomain where the format is something like ```domain.com/123/users``` where 123 is the id of the tenant/account. I had previously implemented this ( before moving to jumpstart) with the format of ```domain.com/accounts/123/users``` which feels a bit long and superfluous having 'accounts' in the url.
I think another nuance with this all would be shifting the billing away from the team and more to the account. I'd actually like to have a basecamp-type subdomain where the format is something like ```domain.com/123/users``` where 123 is the id of the tenant/account. I had previously implemented this ( before moving to jumpstart) with the format of ```domain.com/accounts/123/users``` which feels a bit long and superfluous having 'accounts' in the url.
Teams already cover the multitenancy functionality it sounds like, so maybe they just need to be renamed to Accounts or Organizations. Seems like the confusion is mostly the naming of Team.
Whether it's domains, subdomains, or account ID in the script path, that's all easy to do. We're using cookies to set the tenant right now, but we can first look at the domain, subdomain, or script path and fall back to the cookie if none of those are set.
Same with adding logos to the tenant. Just simply need to add an image attachment to the model.
Billing belongs on the tenant for sure. Basecamp, Github, Heroku, etc all have billing on the tenant.
Trying to think if there's anything else I'm missing here.
Whether it's domains, subdomains, or account ID in the script path, that's all easy to do. We're using cookies to set the tenant right now, but we can first look at the domain, subdomain, or script path and fall back to the cookie if none of those are set.
Same with adding logos to the tenant. Just simply need to add an image attachment to the model.
Billing belongs on the tenant for sure. Basecamp, Github, Heroku, etc all have billing on the tenant.
Trying to think if there's anything else I'm missing here.
What if an Organization has many teams, with one "personal" team like users have one personal team. That way 1 team organizations don't need to think about it and if they want to expand to more teams they can do so under the same organisation's subdomain & have meaningful team names. It might be a bit of overhead for 1 team orgs, but it's the same argument for user's personal teams.
Two things I added when I designed this functionality previously:
- Ability to have multiple administrators for a tenant (only one of whom is the cardholder) allowing more than one individual to set up new members.
- Ability for a user to belong to more than one tenant. Use case for this is less common.
+ 1 to what Roberto added, in the product I'm building, I'll need Account, Teams and Users. The Account would be the tenant, and when the user signs up they are added to a default group and marked as an admin ( for that tenant only).
I had originally thought that I'd have a basecamp like subdomain url structure, but I'm not sure what that gets the user, so am rethinking that now.
I had originally thought that I'd have a basecamp like subdomain url structure, but I'm not sure what that gets the user, so am rethinking that now.
Organization/Accounts could even get their users and admins through their teams something like this:
class Organization < ApplicationRecord
has_many :teams
TeamMember::ROLES.each do |role|
has_many "#{role.to_s}_team_members".to_sym,
-> {eval role.to_s},
through: :teams,
source: :team_members
has_many role.to_s.pluralize.to_sym,
-> { distinct },
through: "#{role.to_s}_team_members".to_sym,
source: :user
end
end
Adding an organization_admin role to teams would then just work.
class Organization < ApplicationRecord
has_many :teams
TeamMember::ROLES.each do |role|
has_many "#{role.to_s}_team_members".to_sym,
-> {eval role.to_s},
through: :teams,
source: :team_members
has_many role.to_s.pluralize.to_sym,
-> { distinct },
through: "#{role.to_s}_team_members".to_sym,
source: :user
end
end
Adding an organization_admin role to teams would then just work.
I don't think any major change is necessary on the Jumpstart template. If the app needs a different structure, you can create another model and change the association as you like.
Or just rename the Teams model to "Organizations", "Accounts" ... whatever you need.
https://stackoverflow.com/questions/11924124/how-to-rename-rails-controller-and-model-in-a-project
You can then change the helpers and make them work with the new model name.
Perhaps the documentation can be improved to explain how multi-tenancy can be done in other ways. Or add a migration file sample that changes the model name.
There can also be Jumpstart Pro tutorials for members only, like GoRails.
Or just rename the Teams model to "Organizations", "Accounts" ... whatever you need.
https://stackoverflow.com/questions/11924124/how-to-rename-rails-controller-and-model-in-a-project
You can then change the helpers and make them work with the new model name.
Perhaps the documentation can be improved to explain how multi-tenancy can be done in other ways. Or add a migration file sample that changes the model name.
There can also be Jumpstart Pro tutorials for members only, like GoRails.
I was thinking keeping the subscriptions at the team level and have accounts/orgs only show a dashboard of their teams with totals and links to change their teams subscriptions... which has pros and cons.
The pros I see are that clients with many teams can customize their billing as much as needed and the dashboard would look cool :)
The cons are obviously adding a bit of complexity and mental load to the organizations' admins instead of having subscription tears by org.
Although, i guess you could implement subscription tears by org too on top of this and validate the totals on org but you would have to include the billing module on org and do authorization through orgs. I'm using Pundit for authorization and it's all setup to work on teams so it just makes more sense to keep subscriptions on teams for me, but that's just my case.
The pros I see are that clients with many teams can customize their billing as much as needed and the dashboard would look cool :)
The cons are obviously adding a bit of complexity and mental load to the organizations' admins instead of having subscription tears by org.
Although, i guess you could implement subscription tears by org too on top of this and validate the totals on org but you would have to include the billing module on org and do authorization through orgs. I'm using Pundit for authorization and it's all setup to work on teams so it just makes more sense to keep subscriptions on teams for me, but that's just my case.
Is my assumption correct? If yes do you have any ETA? I am not in a hurry just trying to plan my work.
If you are indeed working on it appreciate if you could let us know the direction you are taking.
This was the only area where I have confusion but I am not at a place to worry about it so did not spend much time on it.
That is awesome, and acts_as_tenant is great :) Back to the point of how we envision this working. In my case I've been thinking of adding nested teams under organizations and extending the invitation system to take that into account. I'd like to know if anybody else is thinking the same thing.
Made some good progress on this last week. It's a little complicated to test, since it's renaming a TON in the Rails app, but I think it's coming along pretty well.
Next step is adding some configuration so you can select whether you want tenants set by session (like we currently do for teams), path (like bascamp), subdomain, or domain.
Also taking this as an opportunity to add CurrentAttributes. 👍
Next step is adding some configuration so you can select whether you want tenants set by session (like we currently do for teams), path (like bascamp), subdomain, or domain.
Also taking this as an opportunity to add CurrentAttributes. 👍
I had no idea that CurrentAttributes existed it sounds awesome! If you upload the work as a branch I can try to help with the tests :)
CurrentAttributes is pretty cool! I like how organized it makes things.
I've pushed up the current work-in-progress here: https://gitlab.com/gorails/jumpstart-pro/-/commits/multitenancy
I haven't added acts_as_tenant yet, but I have renamed all the files and added migrations to rename things to Account. I've also added functionality to do Account switching by domain, subdomain, session cookie, and script path.
One of the hardest things to test will be upgrading an existing app with resources associated with Team. This will need some effort on the user's end to rename the columns from team_id to account_id and to change the associations.
As for acts_as_tenant, I may actually extract some of the features of it and build our own that works on top of Current.account. That part should be optional anyways as you can just use
I've pushed up the current work-in-progress here: https://gitlab.com/gorails/jumpstart-pro/-/commits/multitenancy
I haven't added acts_as_tenant yet, but I have renamed all the files and added migrations to rename things to Account. I've also added functionality to do Account switching by domain, subdomain, session cookie, and script path.
One of the hardest things to test will be upgrading an existing app with resources associated with Team. This will need some effort on the user's end to rename the columns from team_id to account_id and to change the associations.
As for acts_as_tenant, I may actually extract some of the features of it and build our own that works on top of Current.account. That part should be optional anyways as you can just use
current_account.resources
to reference models like you currently are. This will just be beneficial for other things like adding a default scope and throwing an error if an Account wasn't set to make sure you're inside an account when accessing the resource.Teams will be something you can implement underneath an Account if you still want to group users. We may or may not keep that functionality in JSP, I'm not sure. I think it's more specific to the app on how that should work.
No, but hoping to get to it this week. Depends on how finicky it ends up being.
I know they have an acts_as_tenant Sidekiq integration, but don't think they have support for any other background workers. I'm not sure if we'll want to enable that by default if it's not consistent across all of them or not.
I know they have an acts_as_tenant Sidekiq integration, but don't think they have support for any other background workers. I'm not sure if we'll want to enable that by default if it's not consistent across all of them or not.
Is there anything I can help, Chris?
I am very interested in your approach to advancing a multitenant project.
My use case:
· An app similar to webflow or heroku.
· You register as a user.
· A user can create different sites
· The sites are then accessible via domain (subdomain at the beginning, but also with its own domain)
· All other models depend on the site (articles, jobs, profiles, ...)
· Within each site, organizations can be created and other users can be invited. Ex: mytenantsiteN1.com/gorails
· Devise gem should support authentication via tenant in the same user table (following the acts_as_tenant approach).
https://github.com/heartcombo/devise/wiki/How-to:-Scope-login-to-subdomain removing the email index by tenant - email index
· The sites should be able to have their own keys for Stripe and customize the attributes of each site (logo, favicons, colors, etc ...)
If I can help you with something, please tell me.
Maybe a little complex, but it's what I'm working on.
Thanks for the effort Chris!
I am very interested in your approach to advancing a multitenant project.
My use case:
· An app similar to webflow or heroku.
· You register as a user.
· A user can create different sites
· The sites are then accessible via domain (subdomain at the beginning, but also with its own domain)
· All other models depend on the site (articles, jobs, profiles, ...)
· Within each site, organizations can be created and other users can be invited. Ex: mytenantsiteN1.com/gorails
· Devise gem should support authentication via tenant in the same user table (following the acts_as_tenant approach).
https://github.com/heartcombo/devise/wiki/How-to:-Scope-login-to-subdomain removing the email index by tenant - email index
· The sites should be able to have their own keys for Stripe and customize the attributes of each site (logo, favicons, colors, etc ...)
If I can help you with something, please tell me.
Maybe a little complex, but it's what I'm working on.
Thanks for the effort Chris!
I've got a multitenancy branch if you want to see where things are at. I've renamed Teams to Accounts, which has touched every bit of the application, so it's going to be a tough upgrade for anyone who wants to merge these updates.
So far, I have added:
So far, I have added:
- CurrentAttributes
- Account selection by:
- Path like Basecamp
/:account_id/projects
- Domain
- Subdomain
- Session cookie (same as how Teams switched before)
- Path like Basecamp
- Tests for the different account selection types
Need to add:
- Adding multitenant selection options in the Jumpstart Admin
- acts_as_tenant or activerecord-multitenant
- Haven't decided if one is better than the other (AR-multitenant is a fork of acts_as_tenant)
- I have a half-finished tenant_scaffold generator written. It's like a Rails scaffold but would generate models with account_id and acts_as_tenant already installed. Rails generators are confusing as hell though, so I probably won't do this from the beginning.
- Default of session cookie account switching
- Possibly change accounts#switch to redirect to domain, subdomain, path if enabled
- It also may be safest to just always have session cookie and let that be overridden as needed
- Adding domain / subdomain fields to Account new/edit form
- Possibly adding domain / subdomain fields to registration
- This is a bit tricky with the personal account
- Do personal accounts require a domain / subdomain?
Probably some other considerations to take into account that I've forgotten about, but it's getting close.
Well done Chris!! Just trying it out now and have subdomains working 🥳
I just commented out
Also what are the steps to enable account path like /:account_id/projects ?
My best guess is something like....
I just commented out
if Jumpstart::Multitenancy.subdomain?
in set_current_request_details.rb to get it working. What's the best way to set these configs for now?Also what are the steps to enable account path like /:account_id/projects ?
My best guess is something like....
- Generate scaffold for Projects with account:references
- Nest projects within accounts in routes file?
- Update projects controller to set account?
Very excited about this! 👏👏
Just add the following to your
config/jumpstart.yml
multitenancy: - domain - subdomain - path
The path option is what will do
/:account_id/projects
The account ID is automatically stripped out in the middleware, so your app doesn't have to change. If I remember right, it's just
root_url(script_name: "/#{current_account.id}")
I'll be updating the account switch method to redirect to the /:account_id URL if you have that enabled. Same for subdomain. Otherwise, it will use the session.
Domains won't get a redirect out of the box, because you can't guarantee the custom domain is configured properly, so that's a bit unique. I'll leave a line you can uncomment if you do want that, but I wouldn't recommend it.
You could, but that's pretty darn tricky. You can easily screw up because it's impossible to tell the difference between your app's routes and your tenant routes then since they're all strings. That's why the IDs work best since they're numbers and most routes in Rails won't start with all numbers.
Pushed up domain settings for the Jumpstart config admin as well and several other small improvements.
That's probably wrapping up the majority of these features for multitenancy too.
activerecord-multi-tenant seems strictly tied to Postgres since it's from Citus, so for now I think acts_as_tenant is the more compatible option since several people have converted JSP over to MySQL. This should make sure it stays compatible.
acts_as_tenant is now installed and configured to set the tenant every request, so if you add it to your models associated with:
That's probably wrapping up the majority of these features for multitenancy too.
activerecord-multi-tenant seems strictly tied to Postgres since it's from Citus, so for now I think acts_as_tenant is the more compatible option since several people have converted JSP over to MySQL. This should make sure it stays compatible.
acts_as_tenant is now installed and configured to set the tenant every request, so if you add it to your models associated with:
class Project < ApplicationRecord acts_as_tenant(:account) end
Then you should be good to go. 👍
Long-term I'll probably add in a tenant_scaffold as I mentioned, but I don't know that I'll include it for this initial version.
Should be able to cut a release soon with all these changes. If you guys want to give this a try and let me know how it goes, please do! Once it's tested a bit, I'll merge it into master.
Project.all
will automatically scope to to the current account. If none is set, it will return all of them, but you can also enable the acts_as_tenant config option to throw an error if it's nil. That's maybe a good default we should have, but I'm going to leave it out for now unless everyone is typically using it.Long-term I'll probably add in a tenant_scaffold as I mentioned, but I don't know that I'll include it for this initial version.
Should be able to cut a release soon with all these changes. If you guys want to give this a try and let me know how it goes, please do! Once it's tested a bit, I'll merge it into master.
I just started a new app today, noticed this change, and pulled the changes. Things were working well before, but now when I log in I get ActionController::InvalidAuthenticityToken
↳ app/controllers/concerns/set_current_request_details.rb:29:in `block (2 levels) in <module:SetCurrentRequestDetails>'
↳ app/controllers/concerns/set_current_request_details.rb:29:in `block (2 levels) in <module:SetCurrentRequestDetails>'
skip_before_action :verify_authenticity_token
(obviously not a good solution)/app/app/controllers/account/passwords_controller.rb:1:in `<main>': Account is not a module (TypeError)
ActionController::InvalidAuthenticityToken
login issue you are having.https://gitlab.com/gorails/jumpstart-pro/-/commit/3ccac2d102785f5d9467673380e32fa4ca167448
The main issue is the update in this file app/controllers/concerns/set_current_request_details.rb
change
prepend_before_action
to before_action
I have been fixing a few issues here and there. I think the main last step is to record a video explaining the upgrade. You'll need to rename
Won't be a super fun merge, but should be the last major one for quite a while.
Once I get that video recorded, I think it'll be good to go and we can fix any other issues in master.
team_id
columns to account_id
and change all references of team to account.Won't be a super fun merge, but should be the last major one for quite a while.
Once I get that video recorded, I think it'll be good to go and we can fix any other issues in master.
Notifications
You’re not receiving notifications from this thread.