To (finally) continue with my blog series on multi-tenant web applications, let's first discuss detecting the tenant. (If you'd like to recap, please check out the Introduction and What is a multi-tenant application posts.)
Examples in this post (and series) will focus on examples written for Ruby on Rails and/or Rack applications.
In a multi-tenant app, each request that comes in can be for a separate tenant. We need two things from the outset: a way to determine what tenant a request is for, and a way to process the request in the context of that tenant.
Unfortunately, the solution isn't clear-cut, and depends substantially on the business needs of the application.
Example: Siloed data within one application
Many applications, such as BaseCamp silo their tenants' data, and a user logged into Company X's BaseCamp account can't mingle X's data with Company Y's. But the user still knows they're going to BaseCamp and using that application. Assuming that each user belongs to one tenant, we could easily use the logged-in user as our determining factor.
Example: White-labeled app
Suppose your application is completely white-labeled. Individual users can't know that Tenant A's site and Tenant B's are really powered by the same backend that you're building. In a case like this, the tenant needs to be distinguished from the URL alone, so that the application can always serve customized content, even for requests from users that aren't logged in, or maybe don't have an account. For partial white-labeling, we could distinguish on subdomain of the request (e.g.
tenant_slug.myapp.com, or a parameter after the domain (
www.myapp.com/tenant_slug). To fully white-label, we'd have to point each of our tenants' unique domains at our application, perhaps using a subdomain (e.g.
myapp.tenant_two.com, depending on their needs). Obviously this has the downside of requiring changes with the tenant's domain registrar, whereas we could use a wildcard subdomain record on
myapp.com to get the previous approach.
Of course the solution depends greatly on your specific needs, but the two examples above cover the common cases pretty well.
Single login leading to tenants
If, as in the first example above, "tenancy" is more about siloing data than providing a completely custom experience, then the solution is quite simple.
It's common in Rails applications to have a
current_user helper in
ApplicationController; simply add a
current_tenant helper that looks like
def current_tenant current_user.tenant end
(or something like that, as appropriate to your data model).
You might be tempted to have deeply-nested routes in your RESTful application, like
/tenant/:tenant_id/model_name/:model_id. But given that a user belongs to a single tenant, the
/tenant/:tenant_id portion of the route is both redundant and misleading: it gives the user the impression that they could change the
tenant_id and get different data. (For obvious security reasons, hopefully that's not the case!) Instead, simplify your routes to
/model_name/:model_id and determine the tenant from the user. They'll never go wandering off to another tenant, not even accidentally.
Route-based tenant detection for white-label apps
In case you aim to deliver a more comprehensive tenant experience, where the user isn't aware (or is minimally aware) of the fact that they're using a multi-tenant app, you have a couple of choices.
Detection in controller
One is to look at the
request object in your controllers. Your controller might look like
class PublicController < ApplicationController before_filter :find_tenant … private def find_tenant @tenant = Tenant.find_by_slug request.subdomain end end
I've used this in hybrid applications that use tenant subdomains for certain public routes, but determine by logged-in user for most of the application. However, I wouldn't recommend it for a fully white-labeled app; you'll need to use this in too many different places. (And it may not be appropriate for your data storage system, as we'll discuss later in the series.)
Early detection in Rack middleware
To solve the problem of multi-tenancy in a white-labeled app with independent data storage, we turned to Rack.
Let's look at the middleware itself (stripped down to just detection code):
module Rack class MultiTenant def initialize(app) @app = app end def call(env) request = Rack::Request.new(env) # CHOOSE ONE: domain = request.host # switch on domain subdomain = request.subdomain # switch on subdomain @tenant = TENANT_STORE.fetch(domain) # Do some configuration switching stuff here ... @app.call(env) end end end
You'll notice the
TENANT_STORE "hash" in the above code. This could be an actual hash that's somewhere in your application code, but that's not very dynamic and requires a code change (and application restart) to modify. Instead, that code should be replaced with something that can change during runtime – perhaps a
tenants table in your database, or a hash in Redis.
To add this to your app, add the following line to
Stay tuned ….
Now that our app can detect the tenant for a request, we need to do something about it. Next time, we'll discuss strategies for managing SQL data in a multi-tenant app.