Building a multi-tenant app is easy…if you have an apartment!
These days, more and more startups are appearing on the SaaS market. For their apps, they have several development approaches to choose from. And one of the technical models is the multi-tenancy or multi-tenant app. If you’re going to share all your software with your startup customers — either with the ability to have separate data/content and URLs (say, SaaS), or just part of it — you need a multi-tenant app.
This is not the only choice: I should mention other approaches, but let’s just mention them — a comparison of different approaches could be a good topic for the next article.
Here is my list (ordered by implementation complexity):
- URL path-based SaaS. Easiest one. Single domain, single DB, etc. Application-level restrictions for data.
- Multi-tenant SaaS. Medium complexity. Subdomain-based or domain-based. Multiple databases or schemas. Database-level restrictions for data.
- Virtualization-based SaaS (thanks to Docker and friends!). High level of complexity. Subdomain/domain-based. Multiple apps and DB copies. Virtualization-level restrictions for data.
Multi-tenancy
So what is multi-tenancy? Multi-tenancy is a software development architecture approach in which each client gets their own app configuration and data (strictly or softly isolated from other clients). Each “instance” is called a “tenant.”
Over the past few years, I’ve worked on several multi-tenant apps (I mostly did multi-tenancy from scratch). And even now, I’m working on two multi-tenant apps.
Let’s move on from the introduction and theory to the practice, and look at what could be used for multi-tenant apps in the world of Ruby on Rails.
Apartment
This is number one for Ruby on Rails apps. Let’s add it to Gemfile:
gem ‘apartment’
After bundle install
you need to run a generator so you’ll get some basic configuration templates:
bundle exec rails generate apartment:install
Now you have config/initializers/apartment.rb
where you can tweak how you want to use Apartment. The most important things that should be configured are: how “apartment” will know how to identify your tenants for storing data (we’ll assume it’s a PostgreSQL database where each PostgreSQL schema is a separate tenant), and how to show data depending on HTTP requests.
Ok, in one app I’m developing I have a Website
ActiveRecord model with slug
field. Therefore, the first setting looks like this, and each website is a tenant:
config.tenant_names = lambda { Website.pluck(:slug) }
Let’s say I decided to treat any subdomain as the website’s slug. So if I have a Website
with my-awesome-website
slug, then my-awesome-website.example.com
will serve data from my-awesome-data
DB schema. To have this behavior we need:
# require 'apartment/elevators/subdomain'...
Rails.application.config.middleware.use Apartment::Elevators::Subdomain
The third setting you might need is excluding some models that should be shared across all tenants. Like Website
itself from my example:
config.excluded_models = %w{ Customer Website Plan Feature PlanFeature }
Advanced tips
Custom Elevator Class
Ok, we’ve built a subdomain-based multi-tenant app, but what if we need a custom domains feature for our customers? But it still should be accessible from the subdomain as well. Then we need a custom elevator class — similar to what we’ve used above — Apartment::Elevators::Subdomain
.
Elevator class should decide, based on the current request, what tenant (database/schema) should be used. In case we have a domain
field on the Website
model:
Here we’ve created a custom elevator class (in lib/apartment/elevators/active_website.rb
) inherited from Subdomain
elevator and overridden by parse_tenant_name
that should return tenant name based on a request. So first we call super
and save the result in tenant
variable. If we have a website with the domain set up as a requested domain, we’ll return such a slug (tenant). Otherwise, we fall back to subdomain.
Not Found Page for incorrect tenants
Task #2: what if some non-existent tenant was requested? Someone makes a request to no-such-tenant.example.com
, but we don’t have such a database schema. The best thing we could do is to respond with some 404 page. This task is not about apartment
directly, but is closely related.
We’ll enhance our elevator class like this:
What is ::NotFound
you may ask? This is a simple middleware that I use for this purpose. Placed in app/middlewares
it’s written as below:
Task solved — client is happy!
Excluded Subdomains
If you need one or several subdomains to treat as a public tenant (not a client’s) and not to switch on requests for it, you would use the excluded_subdomains
option. This option is available for Subdomain
elevator and its subclasses of course:
Apartment::Elevators::ActiveWebsite.excluded_subdomains = ['app']
Seeding data
If your tenants are already created, you could initialize them with data using db/seeds.rb
. With rails db:seed
it will be applied to each tenant.
But what if you need to initialize a just-created tenant (via Apartment::Tenant.create
) with some data programmatically? Well, you could still do this by switching to tenant and creating some models (or executing the Rake task programmatically).
Look how spree_shared
gem does this for popular spree
gem (the code below is an updated version because spree_shared
doesn’t work with the current Spree version):
If you need seeds to be applied to each tenant, you might write it this way (again, a modified example from spree_shared
):
Wrapping tenant creation into transaction
The next step is wrapping the tenant creation into the transaction block, because we know that every tenant has a corresponding database model (Website
or Customer
), and we want to make sure both the model instance and the tenant were created. This will make our app more transactional, and we won’t have a DB tenant without the corresponding model instance or vice versa.
Service Objects comes to the rescue! For our hosted websites app, we might write something like this:
We need to handle possible exceptions: if a record or tenant wasn’t created for some reason.
Conclusion
Well, as you may see, developing multi-tenant apps is easy enough if you have good tools like the apartment
gem. Let me know in the comments your cases of apartment
appliance. Especially non-standard ones.