How to handle multitenancy without subdomains in Rails 5
In this tutorial, I'll cover the step-by-step process to setting up a multitenant Rails 5 application that doesn't use subdomains. For this tutorial, I'll be focusing on acts_as_tenant (see why I'm not using Apartment below).
Skip to the step-by-step setup guide.
Why shouldn't you use subdomains for multitenant apps?
Well, it just depends. There are pros and cons to each approach.
Pros of using subdomains for multitenant sites
Subdomains can be a good approach when you create each subdomain. This might be the case for an auto dealer who needs a single backend to manage multiple sites (audi.x.com, bmw.x.com, mercedes.x.com, and so on).
If you plan to let users create their own subdomains, subdomains also allow them to point a custom domain to your site. So instead of theirname.yoursite.com, they can use something like xxx.theirsite.com. Is this a dealbreaker for you? It can always be set up as one of your future features, and you can charge extra for it (since it's more expensive for you to manage).
Some people like the way subdomains look. They think that theirname.example.com is professional, while example.com/theirname is not.
Cons of using subdomains for multitenant sites
If you plan to let users create their own subdomains, SSL becomes a pain to manage. In other words, it's difficult to add https://
to every one of your subdomains. You can do this with something called a wildcard DNS certificate, but it's more expensive than the alternative, and it's harder to set up on a platform like Heroku. Since it's 2018, letting sites fall back to http://
just won't cut it – you definitely don't want Chrome to display that your SaaS app isn't secure.
Many companies have made the switch off of subdomains in the last several years, possibly due to this problem. One that comes to mind immediately is Basecamp. Medium is also doing away with subdomains and their custom domain feature and opting house all users' posts on their base URL.
Why acts_as_tenant?
I really like the Apartment gem (see my step-by-step tutorial here), but even the creators of Apartment have expressed concerns about scaling it:
"At this point, I cannot recommend going the Postgres schema approach given the headaches we've seen above. I hope this post helps you all avoid some of the pitfalls that we've encountered and are still digging ourselves out of." – blog post from Influitive, the creators of Apartment
Heroku also recommends against using the Postgres schema approach and have experienced problems with even just 50 schemas:
The most common use case for using multiple schemas in a database is building a software-as-a-service application wherein each customer has their own schema. While this technique seems compelling, we strongly recommend against it as it has caused numerous cases of operational problems. For instance, even a moderate number of schemas (> 50) can severely impact the performance of Heroku’s database snapshots tool, PG Backups. – Heroku docs
I didn't want to worry about these limitations, so I decided to look into the second most popular multitenant Rails 5 gem out there: acts_as_tenant. I think it would be more popular if it had a simpler name. And while the documentation isn't bad by any means, I think it could use a step-by-step tutorial such as this one. So I'm here to provide that.
How to set up acts_as_tenant
-
Add acts_as_tenant to your Gemfile.
gem 'acts_as_tenant'
-
Run
bundle install
. -
Let's separate customer data into
Accounts
.rails g scaffold Account name:string
-
Start your app and create two accounts. Give the first one your name and the second one my name ("Mark").
-
Now, let's create projects.
rails g scaffold Project name:string
-
Run
rails g migrate add_account_to_projects
. -
Inside the file generated by Step 6 (inside the db/migrate folder), change it to this:
# xxx_add_account_to_projects.rb class AddAccountToProjects < ActiveRecord::Migration[5.2] def change add_column :projects, :account_id, :integer add_index :projects, :account_id end end
-
Now, add
acts_as_tenant
inside your Project model since it's a tenant of Account. (When I first used Acts As Tenant, this terminology confused me. I was expecting a tenant to be your customer's whole account. However, it's slightly different:acts_as_tenant(:account)
means that data inside this model belongs to an account.)# project.rb class Project < ActiveRecord::Base acts_as_tenant(:account) end
-
Since we're not using subdomains, we need to set the tenant per request. The best place to do this is inside the application controller, and acts_as_tenant gives us a handy filter for this. Change your application controller to this:
# application_controller.rb class ApplicationController < ActionController::Base set_current_tenant_through_filter before_action :find_current_tenant def find_current_tenant # set to Account.first for now, you'll change this later current_account = Account.first set_current_tenant(current_account) end end
-
The tenant is set to
Account.first
, so all projects will be scoped to the account under your name (we created this in Step 4). -
Create a new project at localhost:3000/projects/new.
-
Now look at localhost:3000/projects.
-
Now change your application controller to reflect the second tenant:
# application_controller.rb class ApplicationController < ActionController::Base set_current_tenant_through_filter before_action :find_current_tenant def find_current_tenant current_account = Account.second # this line set_current_tenant(current_account) end end
-
Take another look at localhost:3000/projects. The project you created in Step 11 should be gone! That's because you're currently viewing the instance for another account. If this works, nice job – you've successfully set up acts_as_tenant.
-
Now, you'll need to find a way to switch your application controller's
current_account
based on who's accessing your app or what URL they're accessing it from. This varies greatly depending on your app, so I'll turn the reins over to you. For instance, if you set thecurrent_account
per user, you could do something like:current_account = current_user.account
Or if you want to pull it off the URL, you could define a parameter in routes.rb and pull from it inside your controller:
current_account = params[:account_id]
You could even use a gem like FriendlyId to create nice routes for your users, so if someone from Google was using your app, they could navigate to example.com/google/projects.
Conclusion
So there you have it: a simple way to set up multitenancy without subdomains for Rails 5 applications. Like I said in my Apartment post, you've now created a multi-tenant application. Only a small amount of people ever have, so you should feel pretty good about yourself.