Skip to main content

35 Multitenancy with the Apartment gem

Episode 47 · March 19, 2015

Learn how to separate your application data into different accounts or companies

Multitenancy


Transcripts

In this episode we're going to cover multitenancy in rails applications. If you're not familiar with multitenancy, it's basically a way to separate or sequester your records based upon the account that's currently being logged in or visible. That can come from a multitude of different ways, and my example here is Freshbooks, if you go to a subdomain freshbooks.com, you have an account there, and you set up this subdomain when you register. You're probably familiar with this, Basecamp used to do it, and a whole bunch of other sites used to do it, and this allows them to take a look at the subdomain, and then all of the records that are ever accessed through that subdomain are users, projects, you name it, through that specific account that's attached to the subdomain. We can do that using a gem called apartment, and in a future pro episode, we're going to do this from scratch so you can understand how this works. Now, apartment is a pretty great gem, it's pretty simple, and you have a simple installation process and you basically create these tenants. These tenants would be the subdomain that you want, so you'll replace tenant name with that, so you'll grab this information from the user when they register, and then automatically create a tenant accordingly. In apartment, it actually goes the extra step and creates a separate PostgreSQL schema, or if you're using sqlite, it will create a separate database for those records, which is really nifty. This separates data out into different databases effectively, within the same system. That's really cool and something that can be a little tricky to set up from scratch. Using this gem it's really helpful to do that. Let's just get started and take a look at what we can do. I'm going to grab the apartment gem from ruby gems, copy that to clipboard, so let's create a new rails application called "Project Management".

rails new project_management

cd project_management

paste into our Gemfile

gem 'apartment', '~> 1.0.0'

bundle install

rails g apartment:install

This will create an apartment config file for us. We can open that up in the config/initalizers/apartment.rb file, and we'll see here that there's a bunch of configuration stuff for us to use. Now, you want to take a look at this and set it up for your application, so this is going to be different for everything, in one case, by default, they use subdomains to split out the tenants, but this isn't actually wired up until we modify the application.rb to add in some middleware. Reading through some of these other ones though, you'll want to take a look at this, so excluded models is going to be important. Most of the time, you're going to want your user records to be global to your application, you won't have users that necessarily belong only to only a certain subdomain. If that's the case, you'll have to set that up differently, but in this case, we're going to set up users so that they're global and they're available on any subdomain. The list of tenant names here is also important, and apartment needs to be able to look all of those up easily and the recommendation here is to pull them out of the database, so imagine we have a user model that has a subdomain attribute on it, and so the user is the one that registers and gets their own subdomain, so you would say: User.pluck :subdomain attribute off of them, this would create that array of all of the subdomains that are available, and apartment knows which subdomains to respond to on the various requests that come in. I'm actually going to comment out these excluded models section real quick, and we'll go create the users for our application right now. Let's generate a scaffold for the user model, and it will just accept an email and a subdomain, we won't do any passwords or authentication around them because this is just our example application. You could use devise for that and it would work just the same way, solong as you add the subdomain attribute on there. We should be able to exclude the user model now, and then the only other thing we need to do to set up subdomains is to add our middleware into the application.rb file there. The line that you want to grab here is the config.middleware.use, and this is going to allow you to intercept those application requests and automatically switch the tenant that's active based on the subdomain, you can do the same thing with domains or the host as well, and then if you want to build something custom to do this, so maybe you want to include both domains and subdomains and have precedents there or something you would be able to do that as well. We're going to paste in just the subdomain one here and take a look at what we've got.

rake db:migrate

We'll see that apartment is going to give us some output here sating that there's no tenants and it can't migrate for no tenants, so what this is actually saying is that every time you run a migration, it keeps the schemas in sync for the various tenants in your application. So the database is separated based on those schemas, and your application has multiple effectively, databases. Your user records are separated out nicely, and they can run on the same system, which is cool. Now that we've created our user's database, and this one is a global one to our application, we've set this up such that it's excluded from being a tenant, it's the one that controls which tenants are available, and our apartment.rb file is going to do that. If we now create one of these in the browser, let's start the rails application, and then go create a user in the browser, we're going to see that nothing is going to work for us. If we do this normally, and create a user and a subdomain, this isn't going to create a tenant in the apartment gem, and the way to do that is to run the Aparment::Tenant.create('tenant_name') method, we need that to actually run after the user is created. I'm going to have

app/models/user.rb

class User < ActiveRecord::Base 
  after_create :create_tenant

  private

  def create_tenant
    Aparment::Tenant.create('tenant_name')
  end
end

Now when we create users, apartment will know to create a tenant based on the subdomain, and we only need to do this once it's created, that will automatically happen, and the other thing they have basically here is Aparment::Tenant.switch('tenant_name). This is called automatically because we've added the middleware in the middle. This middleware is going to look up the subdomain on the user, and then it will go and switch automatically for us, so this is the only line that we need to actually add to our application to handle this, so that's pretty cool.

If we do "[email protected]" and we make a "onemonth" subdomain, we can create this user and that's all that happened. There's nothing else that's going on. The user got created, the tenant got created with aparment, and that's simply it. Now, in development, you can't use the localhost to reference subdomains and you can't use IP addresses either, so if someone registered the lvh.me domain and this actually points to 127.0.0.1. This is actually just going to work like localhost, but because it's a real domain, it allows us to test locally with subdomains, so you want to use lvh.me instead, and when you request that, you'll get the application just as you expected, but now it allows you to do subdomains at the beginning, so you could type in subdomain at the beginning, but you're going to get a "tenant not found" error. That's of course, because the subdomain named "subdomain" is not listed in the apartment config here.

aparment.rb

config.tenant_names = lambda { User.pluck :subdomain }

is called everytime that a subdomain is requested, and it looks to see if it's in this array, and User.pluck :subdomain is just going to give us onemonth right now, so if we type in onemonth here, we'll see that the application actually responds, and we have working subdomains in our app. Now, if we were to add another user, so if we do "[email protected]" and we add the "Gorails" subdomain, then I still show up on the onemonth lvh.me. If we go to the main, we show up, and then also if we go to gorails.lvh.me, it's the exact same output, and that is because these users are not scoped, they are excluded from the multitenancy of the application, so to test out the actual multitenancy here, let's generate a scaffold for project, rails g scaffold Project title, and you're going to notice here that what I did is I just generated a project without any associations, so I didn't add a user id to the project, this is because apartment is going to know which tenant we're in, and it's going to automatically scope things for us, because it's going to insert it into the separate schema, and that is going to allow us to build our application in a way that is modular like that without having to worry about it. As you can see, now when we run rake db:migrate, the existing tenants also get the migrations applied to them, so onemonth and gorails both got projects, and so did the main application. This allows your migrations to run across all these tenants, and the more tenants that you create, the more of these that will run but everyone will stay in sync that way.

Now we can go to the project section of the gorails subdomain and you'll see there's no projects, but let's create one called "screencasts" and let's create another one called "forum", and if we switch our subdomain, we should see that these projects have only stayed in the gorails section, so if we go over to onemonth subdomain, there's no projects there, and that's because they're looking in a separate database, and if you look in our rails logs, you'll see that the projects output from the queries are no different, the setup and connection to the database happen behind the scenes with apartment through that middleware that we're using, and the project.load knows to query these databases separately based upon the subdomain. That's really all we have to deal with in our application on a general level, so you've already got all of your records separated out automatically, and that means you can start building things independently for each subdomain. The one thing that you have to be careful with is that your main domain handles things appropriately. Now, it's wise not to ever use domains without www for many reasons. Some DNS related, but in multitenant applications, it's important to have www in there so that you get things that work correctly.

In this situation here, we're on the onemonth subdomain, I added a project called onemonth rails, but if we go to the normal lvh mean without a subdomain, this is going to load up the first tenant that's available, and that's something that you probably don't want to do. You don't actually want that to be the homepage of your application, so the important thing to do is to redirect the non subdomain one domain to something like www.domain. When you're in production, you want to use the www subdomain especially in multitenant applications, but we need to be able to ignore that in apartment, we don't want users to be able to register the www subdomain and then take over our site. That would be bad. Here we can add this into our initalizers section, and we can add that excluded subdomain. They recommend putting it inside the apartment folder in subdomain exclusions, but I'm just going to paste it here at the bottom of our config/initalizer/aparment.rb file. I just noticed that it adds the middleware in here for a subdomain, so we don't actually need to follow the direction that the README said of putting it inside your application.rb because it's already handled for us here.

This will allow us to exclude the subdomain, and now we can refresh this page after restarting our rails application, so refreshing this page of course we get an empty list of projects, and that means that this is going to store these projects in the main rails database. This would happen just like you're used to. Anytime you're in a subdomain, like gorails, these will be saved in the gorails database schema. They will be separated out, but you'll want to be sure to remember that the www and the other excluded subdomains that you have are going to still have these database tables in them, and these routes will still be available. We can add database or routing constraints to make the projects resource unavailable from the homepage so that they're only available inside of our tenants, that's an optional thing to do, but I'll show you how to do that right now.

Now in this application so far we've only got two resources for our database models, but we're probably going to add a lot more as we make this a full application. I'm going to create a constraints block and we'll use that here to wrap any of those routes that are going to be specifically for subdomains, so our customers will have different routes than our homepage and our main application, so if you sign up, there will be a certain amount of things you can do that you can only do there as part of signup like our marketing stuff, things like that, and then inside the subdomains, there will be a whole section that only applies to subdomains and using the application as an actual customer. We'll set these up in two different blocks here, one is going to be unconstrained, that will be the user section there, and then the constraint on subdomain will be here. I'm going to paste in this REGEX, and this is basically going to negatively match the www subdomain, so it's going to look for any subdomain that isn't www and it will match that saying that the projects resources routes are available if it's not www. Saving this we can go back to Chrome and we can refresh the www /projects and we'll get a routing error now because it no longer matches a route, but if we go to gorails.lvh.me, we'll be able to access that again. On your marketing site, you probably don't need the projects to be available, you probably want to display your landing page, your sign up process, things like that, so having a section here for subdomain only resources is kind of useful and allows you to separate your applications a little bit that way.

Another important refactoring here is that once you start to match more than just www, you're going to run into a slew of problems because this REGEX is going to be unmanageable pretty quickly, so we're going to delete that and we're going to add a class called SubdomainConstraint, and I'm just going to create it right here, but we need to create a method:

routes.rb

class SubdomainConstraint
  def self.matches?(request)
  subdomain = %w { www admin }
    request.subdomain.present && subdomains.include?(request.subdomain)
  end
end

This will be a way to refactor that and put the separate logic out somewhere else. Refreshing our rails app we'll be able to see the project still works on gorails, if we go to www, we get the routing error as we should expect. This subdomain constraint probably will be more useful to have something like the list of subdomains that you want to exclude.

This allows us to reserve some subdomains as well that don't match these projects and things like that, so you want to make sure that this is the same list as the one that you are using in apartment, but you can share that with a constant variable across your application or something like that as well.

The last thing that I want to show you is that if we open up the config/initalizers/session_store.rb file, this is where the cookies are defined in your rails application, so your session is saved to a cookie that's encrypted, and this sets the key so it's the name of the cookie. The important thing here though is that your browser is going to save the cookie by the full subdomain, and that is going to mean that you log in to gorails.lvh.me, and you won't be allowed into onemonth.lvh.me or www, and you probably want to change that so that logging in once applies to all these subdomains. The way to do that is to set the domain to the top level domain, and if you put it to lvh.me without subdomains, then this will set the cookie and the browser accepts that, you couldn't set this for someone else's domain, and your browser will know that ok, you have subdomains will allow you to set a cookie on the global domain or the subdomain specifically, so you get one or the other and you're able to share this session accross the subdomains by setting the domain here. In development you want to set it to lvh.me and in production you want it to match your production domain that you're running on, of course.

That was the rough introduction to the complicated world that is multitenant applications, there's a lot to worry about, you have to understand lots of different small things about browsers, DNS, subdomains, things like that, but all in all the apartment gem does a fantastic job to build your basic multitenant rails application, so in the next pro episode, I'm going to take a look at how to build this on a basic level from scratch and how you can implement this without database separation if that's something you're not too worried about

Discussion