Please, stop using Apartment!
Before that, what is a multi-tenant app?
According to Wikipedia:
The term "software multitenancy" refers to a software architecture in which a single instance of software runs on a server and serves multiple tenants.
Multitenancy usually means handling many independent tenants from one app. A tenant is a group of users who share a common access with specific privileges to the software instance.
Most popular in SAAS models and, there are several types of implementation.
I found this article very helpful about the different approaches to multitenancy in Rails apps, please read it.
In my case, i have a CMS builder, where user can create their own blog, customize it, etc. This user can invite people to work on, the blog has an authentication module for their readers and so on.
The reader access to the blog thought a subdomain. Like, something.blogapp.com
I think with this example it is quite clear.
Whats wrong with Apartment Gem?
As i said in the beginning of this post, Apartment Gem is not an option anymore. I repeat it because, when i looking for information before solve the problem, i found a lot of current blog posts where people still are using/recommending it.
Apartment Gem are based on Schema-level approach. If you read the article i mention before, you now know about it, if no:
Every tenant has a separate namespace. In practice, we are creating more tables like:
- tenant1.users
- tenant2.users
- tenant3.users
Let's see the pros & cons if this approach:
Pros:
- No data leakages between tenants
- Easy extraction of single-tenant data
- No additional costs
Cons:
- Less known PG feature & stateful search_path
- A growing number of database tables. Heroku is against this approach when you are using their services (Heroku about Multiple Schemas)
- Migrations run in O(n) complexity
- Trouble on managed databases
- Slower (create a schema, create tables)
Coming back with Apartment:
- No support for Rails 6.0
- Gem no longer supported
- Code is not written optimally, found couple performance errors
- Creators are not even encouraging it (Our multitenancy journey with Postgress schemas and Apartment)
- Sometimes migrations are not run in some namespaces which leads to an inconsistent data structure
Summary: An earlier solution for multitenancy in Rails is still widely used even though it is not optimal in contemporary times.
- Credits: you will find a guide to migrate from Apartment to ActiveRecord Multi Tenant (another gem) too
Finally, how i solve it
I choose the Row-level approach. With this, you have a tenant_id column in every DB table and filter by tenant_id in each query.
Pros: It's just another row in the table
Cons: It's just another row in the table 😂, i mean, you need to have tenant_id keys everywhere and if you forget the WHERE condition, you have leaking data.
I'm using Devise so, the tenant_id is my user_id (the same with Readers, i will skip that to avoid make it more longer). I also add a current_blog to the users table, to allow user have multiple blogs, and change between ones. This is how the User model looks like:
class User < ApplicationRecord
belongs_to :current_blog, class_name: 'Blog', optional: true
end
Blog model,
The migration: $ rails g model Blog name:string:uniq owner:references
class Blog < ApplicationRecord
belongs_to :owner, class_name: "User"
has_many :readers, dependent: :destroy
end
Keep in mind: this is the final result, if you try to make a relationship with :readers before the table exits, you will get an error. Run all the migrations first.
Reader model
The migration: $ rails g model Reader username:string:uniq blog:references
class Reader < ApplicationRecord
belongs_to :blog
validates_uniqueness_of :username, scope: :blog_id
end
When i want to get the readers of the blog, i just add the tenant references to get the resource:
@readers = Blog.where(user: current_user.current_blog).readers
Subdomains
I solve it with constraints.
This is how my /config/routes.rb
looks like:
require "subdomain.rb"
Rails.application.routes.draw do
constraints(Subdomain) do
scope module: :readers do
root "readers#show", as: :subdomain_root
end
end
# * everything else...
end
end
The /lib/subdomain.rb
:
class Subdomain
def self.matches?(request)
case request.subdomain
when 'www', '', nil
false
else
true
end
end
end
Then just i find the blog with a callback on my application_controller.rb:
class ApplicationController < ActionController::Base
before_action :set_blog_if_subdomain
def set_blog_if_subdomain
if request.subdomain.present?
@blog = Blog.find_by_name(request.subdomain)
render_404 if !@blog
end
end
def render_404
render file: "#{Rails.root}/public/404.html", status: 404
end
end
To test it on development, run the rails server with: $ rails s -p 3000 -b lvh.me
, then you can access to http://blog-name.lvh.me:3000
In production you need a Wildcard. You can generate one for free with Let's Encrypt.
Just upload the cert (including the root domain) to Heroku and point everything on your DNS.
A CNAME record to: *, herokudns
and an CNAME record to the root domain: @, herokudns
.
Bye 👋
Discussion (6)
Have you tried github.com/ErwinM/acts_as_tenant ?
What are your thought?
I didn't try it, but it definitely seems like a good option. It is similar to the rudimentary way in which I solved it in my project, but it raises the scopes, and stores everything in concerns, leaving a much fancier code.
You must give credits to the original writers : realptsdengineer.com/ruby-on-rails...
Just to who still need the Apartment gem - there is a fork that is actively maintained and working with Rails 6: github.com/rails-on-services/apart...
Thank you!!
Thanks for this! Is there anywhere we can see the source code for this on Github?