We've moved discussions to Discord

Multitenancy in Jumpstart Pro: How do you envision it working?

Chris Oliver
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?
Dan Weaver
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
  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
...
  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.
Dan Weaver
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.
John Chambers
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).

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.



Willard Moore
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. 
Will Barker
How do you want tenants to be looked up?

By subdomain or domain.  

so would each team be the tenant?

Yep that makes sense.

Scoping
Agree multiple postgres schemas is not the way to go. Seems to cause more problems than it fixes.  
Ugurcan Kaya
+1  Will Barker  
Ryan Chin
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.
Sean Christman
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. 
Matt Bjornson
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. 
Chris Oliver
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.
Roberto Plancarte
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. 
Sean Christman
I think you should rename Team to Account, and let Account be the tenant. Teams aren't difficult to implement separately, and I wouldn't be disappointed if that was just left up to the end developer to sort out.
Mark Nelson
Two things I added when I designed this functionality previously:

  1. 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.
  2. Ability for a user to belong to more than one tenant. Use case for this is less common.
Matt Bjornson
+ 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.
Roberto Plancarte
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.
Ugurcan Kaya
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.  

Roberto Plancarte
+1 Ugurcan

It seems that each app has different needs that can't all be taken into account with just one solution. Documenting or even branching the repo to have different options would be cool. That way each sub-community can maintain their multi-tenancy implementation. 
Matt Bjornson
Roberto Plancarte  Thanks for sharing that. I think the only challenge with that is where is the subscription owned... will that be then with the org?
Roberto Plancarte
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.  
Chris Oliver Based on your comments in this thread and some other it seems like you are working on some form of Multi Tenant functionality. 

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.
Chris Oliver
A C  Yep, working on it. If you build against Team as the tenant model, it should make upgrading to the multitenant stuff easier when I get that updated. We will probably keep everything the same but rename Team to Organization or something.
Chris Oliver  So if I understand correctly a organization (earlier teams) has team members and the application resources can be assigned to either the team members for private access and to an organization (team) for shared access. Am I correct? 

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.
Chris Oliver
Every user gets a personal team that only they belong to which can be used for keeping resources private to the user.

That way you can always assign resources to a team, and it's the team that determines if it's shared or personal.
Roberto Plancarte
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. 
Roberto Plancarte  I am also thinking of doing something similar. An organization will have nested teams. I am currently thinking of restricting to 50 teams per organization and teams can be nested 3 levels deep.
Matt Bjornson
Roberto Plancarte   A C  I'm also planning on having teams associated with accounts ( the tenant) of which there will be multiple teams and team members can invite others to their team. For the first user of a new account, I'd like an admin group to be created and that first user added to that group.
Awesome Matt Bjornson . It looks like most of us are wanting similar things. Lets wait for Chris Oliver to complete the work he is working on and then we can take it from there.
Chris Oliver
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. 👍
Roberto Plancarte
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 :)
Jim Jones
I still don't think it's clear - how will Multitenancy differ from teams?  

Is Multitenancy applicable for reseller accounts for the given Saas app?  Each Top-Level multitenant has their own set of users (and those users have teams)?
Chris Oliver
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 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.
Chris Oliver
Jim Jones It will be the same. Teams are being renamed to Accounts. Then you can either select the current account in the navbar (as you currently do with Teams) or you can use the domain, subdomain, or script path to select the current account if you would like. 

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.
Matt Bjornson
Chris Oliver  seems like you are making great progress on this, super excited about this update! Do you have an eta on when you'll have the acts_as_tenant functionality integrated? 
Chris Oliver
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.
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!
Sean Christman
Just wanted to bump this and see how progress is coming along.
Chris Oliver
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: 
  • CurrentAttributes
  • Account selection by:
    • Path like Basecamp /:account_id/projects
    • Domain
    • Subdomain
    • Session cookie (same as how Teams switched before)
  • 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.
John Chambers
Well done Chris!! Just trying it out now and have subdomains working 🥳

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! 👏👏
Chris Oliver
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.
Chris Oliver
And I'm going to leave session switching as a fallback and always enabled. The other options will hit first, so it won't get used unless the others are disabled.
John Chambers
Sweet! Thanks Chris Oliver  
Account paths now working perfectly /:account_id/projects

So potentially I could change the account middleware to also work for paths like  /:account_subdomain/projects ?

Chris Oliver
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.
John Chambers
Ahh I see! something for me to ponder! 

Guess I could probably setup a list of excluded subdomains if it was really required 

validates :subdomain, exclusion: { in: %w(users accounts announcements charges),    message: "%{value} is reserved." }
Chris Oliver
Yeah, I'm going to be adding a constant of reserved subdomains, just like this basically.
Chris Oliver
And... pushed those up as well as some improved tenant switching tests.
Chris Oliver
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:

class Project < ApplicationRecord
  acts_as_tenant(:account)
end

Then you should be good to go. 👍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.
Tyler Smart
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>'
John Chambers
I'm having the same issue. Haven't figured out how to resolve it yet.
John Chambers
Tyler Smart I have temporarily disabled CSRF in application_controller.rb just so I can continue development for the meantime skip_before_action :verify_authenticity_token (obviously not a good solution)

Chris Oliver I'm getting this error now when I push to a production server
/app/app/controllers/account/passwords_controller.rb:1:in `<main>': Account is not a module (TypeError)

John Chambers
Tyler Smart   Chris has just pushed an update for the 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
Tyler Smart
I got it, works. Thanks!
Great, great work Chris!!!!

Works for me. 

Thank you very much 👍 
Chris Oliver
Manuel awesome 🙌

Anyone else had a chance to try it?
Christopher Wavrin
It all seemed to work for me. :) 
Thanks!
Jim Jones
Chris Oliver Any thoughts on when this will get merged into master?
Chris Oliver
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 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.
Jim Jones
Got it.  I have a site that I've built but I've been holding off on making other changes.  I think this large change may alter how I approach future features, so I'm waiting to see.
Chris Oliver
Pushed multitenancy live this morning and just sent out an email announcement about it.

Here's a screencast on the changes:
Notifications
You’re not receiving notifications from this thread.