Using Subdomains in Rails Apps

This article will show you how to use URLs like http://photosByBetty.fancysite.com instead of http://www.fancysite.com/artists/photosByBetty in your Rails application.

Examples of where you might want to do this include blogs for site members, galleries for artists, chapters of a non-profit organization, teams of a sports league, or even different retail locations for a store. Using subdomains for these types of site delineations allows URLs to be shorter and have a more professional appeal. Taken further, it can also provide an opportunity to to make each subdomain site look customized while running from the same application code.

Organizing a Rails application to use subdomains like this is actually quite easy to do. It involves three major pieces: a way to configure your web server to accept customized subdomains, a way to determine what subdomain your application has been called with, and a way to retrieve data specific to that subdomain.

Configuring the Web Server

I’m afraid I only know Apache well enough to provide details for it as the HTTP server. I’m going to assume you already know how to modify your Apache setup (either directly in httpd.conf, or through a GUI admin tool), or that you have someone who can do it for you. Typically, in the virtual host definition for your site, you’ll find at least the first line, and probably something like both of these:

ServerAdmin fancysite.com
ServerAlias www.fancysite.com

The first line establishes the base domain for your site. The second line says that any request for www.fancysite.com is an alias, and is therefore the same as a request to, fancysite.com. Thankfully, it is not necessary to edit Apache every time there’s a new subdomain for your site. Instead you can accept requests for any subdomain using a wildcard like this:

ServerAdmin fancysite.com
ServerAlias *.fancysite.com

Using that config setup, Apache will recognize any subdomain as a legitimate request to your site. This includes subdomains that actually are not legitimate, but it’s quite easy to deal with those inside our Rails app, so we’ll cover that a little later.

Edit your site setup to use the above wildcard ServerAlias directive, and restart Apache.

Defining Subdomains for Local Development

When you’re developing, you’re going to need your local browser to understand these subdomains too. That’s a little more tedious, but still easy to do. All the major OSs have a file named hosts. For most *nix OSs including OS X, this is located in /etc/. For Windows, it should be in the SYSTEM\system32\drivers\etc directory. (SYSTEM might have different names for the various versions of Windows). In all cases this is a simple text file which can be edited by your source code editor. You can add entries at the end of this file, but don’t edit anything that is already there. Extra blank lines are OK. You’ll need to add entries for each subdomain you want to use for developing and testing on your local system. You want to have at least two to prove that your application really can use the data for unique subdomains. The entries will look something like this:

127.0.0.1    www.fancysite.dev
127.0.0.1    photosByBetty.fancysite.dev
127.0.0.1    petersPaintings.fancysite.dev

Oh yeah, I always use .dev for my local site testing, but you can do whatever you normally do.

OK, so now, in your browser if you were to type in any one of these subdomain URLs, your application should load up. Now, Apache will accept any subdomain, but without the benefit of DNS, we’re limited on the test machine to domains entered in the hosts file.

Detecting Subdomains in Rails

With your application loading up with these subdomains, we now need to determine which subdomain the site is running as. This is really easy.

In the application.rb file, add this line near the top of the file:

@current_subdomain.(self.request.subdomains.join('.'))

What this line does is takes Rails' request object, asks for the subdomains instance variable (which is an array of all subdomains that were separated by periods), and then joins them back into a single string with the periods again. This will work for the majority of cases, but here is where you can take a little license. I’ve done work with state and county governments in the US, so it would be common to have county.state.fancysite.gov URLs. I consider the full county.state string to be the subdomain name. You can monkey with the subdomains array to extract what you need if necessary. If you know you’ll never have multiple subdomains, then you can probably use this:

@current_subdomain.(self.request.subdomains[0])

Exactly where you add it might matter depending on what all you have in your application.rb file. Generally, you’re going to want this instance variable set as early in application.rb as possible. Chances are much of your application will depend on knowing what the subdomain is, so the sooner you have that set the better. If you have an initialize method in application.rb you probably want to have @current_subdomain defined in there. Or if you have a before_filter method, then that would be a good place too.

You’ll notice we did not create any custom routes for Rails. It just isn’t necessary.

Using current_subdomain to Customize the App

With @current_subdomain, we can now make decisions in the application. Obviously doing this using if or case statements would be a total bore. So, a smarter way to handle this is to create a data table with fields for each of the attributes of your site that will be unique based on the subdomain. If you’re running a hosted service application like a gallery site, or blog, this may in fact turn out to be the very same table you use to hold client data. If you’re running a site for an organization with multiple chapters or locations, then this data might just come from there.

Let’s assume there’s a table called clients in which we have the following fields: site_title, email_contact, and copyright_notice.

In application.rb, we could add a new method called get_subdomain_details. We would add this method as a before_filter so that it gets called for every action of every controller (you can of course set up the filters to behave the way your site needs).

In get_subdomain_details, you’re going to instantiate the Client model, then use the find method to retrieve the record for the Client whose subdomain == @current_subdomain. Our application.rb file might look something like this:

class ApplicationController < ActionController::Base

before_filter :get_subdomain_details

def get_subdomain_details
    @current_subdomain.(self.request.subdomains[0])
    @this_subdomain = Client.load_subdomain_details(@current_subdomain)
end
end
class Client < ActiveRecord::Base

def load_subdomain_details(current_subdomain)
    find(:first,
        :select => 'site_title, email_contact, copyright_notice,
        :conditions => ["subdomain=':subdomain'", {:subdomain => current_subdomain}])
end
end

Now, for every request, you will have an object @this_subdomain which can be used to populate the specific details of the site that have to be unique. Obviously we’re going to use @this_subdomain.site_title to populate the title tag for the page, we could use @this_subdomain.copyright_notice in the page footer, etc.

What About Invalid Subdomains?

So what happens when someone enters bogus.fancy.com as the URL and bogus doesn’t exist in your table of valid subdomains? You’ll need to add more code to load_subdomain_details to catch the case where no records are found in that find step. Then, it can return a false, or some other signal in the manner that you prefer, to indicate the request is not valid. Chances are the appropriate thing to do is show a page similar to a Page Not Found, or default to the basic www domain of your site with maybe an extra message saying the subdomain site the user requested doesn’t exist. At any rate, you can trap for this situation pretty easily, and create a page display you believe is suitable for your site.

Wrapup

With this method, your application can automatically use subdomains as soon as they’re added (and probably activated) to your database. There’s details you can adjust as needed, but I hope this helped get you started.