Read my latest article: Launching Rails projects, an open call for lessons learned (posted Tue, 23 Jun 2009 17:33:00 GMT)

Subdomain accounts with Ruby on Rails explained

Posted by Robby Russell Sun, 11 Jan 2009 23:30:00 GMT

18 comments Latest by forex trading tools Fri, 15 May 2009 11:23:34 GMT

DHH recently posted, How to do Basecamp-style subdomains in Rails on SvN and it just happens that I was implementing some similar stuff this last week for a project we’re developing internally.

In our project, not everything needs to be scoped per-account as we are building a namespace for administrators of the application and also want a promotional site for the product. Three different interfaces, with some overlap between them all.

Let’s walk through a few quick steps that you can follow to setup the two interfaces within the same application.

Suppose that we’re going to build a new web-based product and have the following requirements initially.

  • We need a promotional site for sign-ups, frequently-asked-questions, support requests, etc.
  • When people sign-up for an account, they’ll should have their own unique sub-domain
  • There are two different visual layouts (promotional site and the account)

Note: I use RSpec and am going to skip the TDD process here and let you conquer that for yourself. Am using the default Rails commands in this tutorial.

Account model / Database

We’re going to generate a new model for Account, which will be responsible for scoping sub-domains and individual accounts.

  account-demo : ruby script/generate model Account subdomain:string 
       create  app/models/
       create  test/unit/
       create  test/fixtures/
       create  app/models/account.rb
       create  test/unit/account_test.rb
       create  test/fixtures/accounts.yml
       exists  db/migrate
       create  db/migrate/20090111220627_create_accounts.rb

Great, let’s migrate our application.

   account-demo : rake db:migrate
  ==  CreateAccounts: migrating =================================================
  -- create_table(:accounts)
     -> 0.0045s
  ==  CreateAccounts: migrated (0.0052s) ========================================

Before we get too far, let’s make sure that we’re adding an index on this table for the subdomain, as it’ll improve performance in the database as the subdomain will used in SQL conditions quite often.

account-demo : ruby script/generate migration AddIndexToAccountSubdomain
     exists  db/migrate
     create  db/migrate/20090111221009_add_index_to_account_subdomain.rb

Let’s open up this new migration file and toss in a UNIQUE INDEX on subdomain.

  class AddIndexToAccountSubdomain < ActiveRecord::Migration
    def self.up
      add_index :accounts, :subdomain, :unique => true
    end

    def self.down
      remove_index :accounts, :subdomain
    end
  end  

Okay, let’s migrate this bad boy.

   account-demo : rake db:migrate
  ==  AddIndexToAccountSubdomain: migrating =====================================
  -- add_index(:accounts, :subdomain, {:unique=>true})
     -> 0.0047s
  ==  AddIndexToAccountSubdomain: migrated (0.0050s) ============================  

Great, we’re now ready to move on to the fun stuff.

Let’s open up app/models/account.rb and throw some sugar in it.

Data Validation

Because we’re going to be dealing with subdomains, we need to make sure that we’re only allowing people to sign-up with valid data otherwise, there could be issues. URLs need to fit within certain conventions and we need to make it as graceful as possible for our customers.

Let’s make a quick list of what we need to enforce for the subdomain attributes. This can easily be expanded, but let’s cover the basics.

  • Each account should have a subdomain
  • Each subdomain should be unique within the application
  • A subdomain should be alpha-numeric with no characters or spaces with the exception of a dash (my requirement)
  • A subdomain should be stored as lowercase

So, let’s update the following default Account model….

  class Account < ActiveRecord::Base
  end  

..and add some basic validations.

  class Account < ActiveRecord::Base
    validates_presence_of :subdomain
    validates_format_of :subdomain, :with => /^[A-Za-z0-9-]+$/, :message => 'The subdomain can only contain alphanumeric characters and dashes.', :allow_blank => true
    validates_uniqueness_of :subdomain, :case_sensitive => false

    before_validation :downcase_subdomain

    protected

      def downcase_subdomain
        self.subdomain.downcase! if attribute_present?("subdomain")
      end
  end  

Reserved subdomains

In the project that our team is working on, we wanted to reserve several subdomains so that we could use them later on. We tossed in the following validation as well.

  validates_exclusion_of :subdomain, :in => %w( support blog www billing help api ), :message => "The subdomain <strong>{{value}}</strong> is reserved and unavailable."

This will prevent people from using those when they sign up.

Controller / Handling Requests

Let’s now think about how we’ll handle requests so that we can scope the application to the current account when a subdomain is being referenced in the URL.

For example, let’s say that our application is going to be: http://purplecowapp.com/ [1]

Customers will get to sign-up and reserve http://customer-name.purplecowapp.com/. I want my account subdomain to be green.purplecowapp.com and everything under this subdomain should be related to my instance of the application.

I’ve begun working on my own module, which is inspired mostly by the account_location plugin with some additions to meet some of our product’s requirements.

Here is my attempt to simplify it for you (removed some other project-specific references) and have put this into a Gist for you.

  #
  # Inspired by
  # http://dev.rubyonrails.org/svn/rails/plugins/account_location/lib/account_location.rb
  #
  module SubdomainAccounts
    def self.included( controller )
      controller.helper_method(:account_domain, :account_subdomain, :account_url, :current_account, :default_account_subdomain, :default_account_url)
    end

    protected

      # TODO: need to handle www as well
      def default_account_subdomain
        ''
      end

      def account_url( account_subdomain = default_account_subdomain, use_ssl = request.ssl? )
        http_protocol(use_ssl) + account_host(account_subdomain)
      end

      def account_host( subdomain )
        account_host = ''
        account_host << subdomain + '.'
        account_host << account_domain
      end

      def account_domain
        account_domain = ''
        account_domain << request.domain + request.port_string
      end

      def account_subdomain
        request.subdomains.first || ''
      end

      def default_account_url( use_ssl = request.ssl? )
        http_protocol(use_ssl) + account_domain
      end

      def current_account
        Account.find_by_subdomain(account_subdomain)
      end

      def http_protocol( use_ssl = request.ssl? )
        (use_ssl ? "https://" : "http://")
      end
  end  

View gist here (embed wasn’t working right when I tried)

Just include this into your lib/ directory and require it in config/environment.rb. (if people think it’s worth moving into a plugin, I could do that)

Including AccountSubdomains

In the main application controller (app/controllers/application.rb), just include this submodule.

  class ApplicationController < ActionController::Base
    include SubdomainAccounts

    ...
  end  

Now, we’ll want to add a check to verify that the requested subdomain is a valid account. (our code also checks for status on paid memberships, etc… but I’ll just show a basic version without that)

Let’s add in the following to app/controllers/application.rb. This will only check on the status of the account (via subdomain) if the current subdomain is not the default. For example: purplecowapp.com is just our promotion site, so we won’t look up the account status and/or worry about the subdomain. Otherwise, we’ll check on the status.

  before_filter :check_account_status

  protected
    def check_account_status
      unless account_subdomain == default_account_subdomain
        # TODO: this is where we could check to see if the account is active as well (paid, etc...)
        redirect_to default_account_url if current_account.nil? 
      end
    end  

Current Account meets Project model

When requests are made to an account’s subdomain, we want to be able to scope our controller actions.

WARNING: I’m going to gloss over the following steps because this is just standard Rails development stuff and I want to focus on how to scope your Rails code to account subdomains.

I’ll just say that this product gives each account many projects to do stuff within. I’ll assume that you’ll know how to handle all that and we’ll assume you have a Project model already.

What you will need is to add a foreign key to your table (projects in this example) that references Account. So, make sure that your model has an account_id attribute with and that the database table column has an INDEX.

We’ll add our associations in the models so that they can reference each other.

  # app/models/account.rb
  class Account < ActiveRecord::Base
    has_many :projects
    # ...
  end

  # app/models/project.rb
  class Project < ActiveRecord::Base
    belongs_to :account
    # ...
  end  

Okay great… back to our controllers. The SubdomainAccounts module provides you with the current_account variable, which you can use within your controllers/views. This allows us to do the following in our controllers. For example, if we had a ProjectsController.

  class ProjectsController < ApplicationController
    def index
      @projects = current_account.projects.find(:all)
    end

    def new
      @project = current_account.projects.new
    end 

    def show
      @project = current_account.projects.find(params[:id])
    end

    # ...
  end

See, this wasn’t so hard, was it?

Handling layouts

I wanted to highlight one other thing here because I suspect that most projects that fit this will likely need a promotional/resource site where people will sign-up from. In our application, we have two application layouts. One for the main application that customers will interact with via their subdomain and the promotional site layout.

The default layout is just app/views/layouts/application.html.erb and we have our promotional site layout at app/views/layouts/promo_site.html.erb. A few of our controllers are specifically for the promotional site while the rest are for the application itself and in some cases, there is some overlap down to individual action within a controller.

What we did was add a few more before filters to our application controller to a) define the proper layout to render, and b) skip login_required on the promo site.

To have the proper layout get rendered, we’re just checking whether the current request was made to the promotional site or not.

class ApplicationController < ActionController::Base
  # ...
  layout :current_layout_name # sets the proper layout, for promo_site or application

protected

  def promo_site?
    account_subdomain == default_account_subdomain
  end

  def current_layout_name
    promo_site? ? 'promo_site' : 'application'
  end

  # ...
end

Our application is using Restful Authentication and we just want to check to see if the current request is made to the promotional site or not. If it is, we’ll skip the login_required filter. Let’s assume that you have the following before_filter set.

class ApplicationController < ActionController::Base
  # ...
  before_filter :login_required

We’ll just change this to:

class ApplicationController < ActionController::Base
  # ..
  before_filter :check_if_login_is_required 

  protected
    def promo_site?
      account_subdomain == default_account_subdomain
    end

    def current_layout_name
      promo_site? ? 'promo_site' : 'application'
    end    

    def check_if_login_is_required
      login_required unless promo_site?
    end

    # ...

There we go. We can now render the proper layout given the request and only handle authentication when necessary.

Development with account subdomains

When you begin developing an application like this, you need to move beyond using http://locahost:3000 as we need to be able to develop and test with subdomains. You can open up your /etc/hosts (within a Unix-based O/S) file and add the following.


127.0.0.1 purplecowapp.dev
127.0.0.1 green.purplecowapp.dev
127.0.0.1 sample.purplecowapp.dev
127.0.0.1 planetargon.purplecowapp.dev
127.0.0.1 lollipops.purplecowapp.dev
127.0.0.1 help.purplecowapp.dev
127.0.0.1 support.purplecowapp.dev

After you edit that file (with root permissions), you can flush your dns cache with dscacheutil -flushcache (Mac OS X). This will let you make requests to http://purplecowapp.dev:3000/ and http://green.purplecowapp.dev:3000. This is a convention that our team has begun using for our own projects (TLD ending in .dev). It’s important to remember that the subdomain must be specified here in order to work for local requests. Unfortunately, hosts files don’t support wildcards (’*’).

Update

You can also use Ghost, which is a gem for managing DNS entries locally with Mac OS X. Read Get to know a gem: Ghost

Summary

I know that I glossed over some sections, but was hoping that the code itself would be the most beneficial for you. Feel free to leave any questions and/or provide some feedback on our approach. Perhaps you have some suggestions that I could incorporate into this so that we can improve on this pattern.

1 yeah, I’ve been reading more Seth Godin recently…

Subscribe to my RSS feed Enjoying the content? Be sure to subscribe to my RSS feed.
Comments

Leave a response

  1. Avatar
    Joe Noon Mon, 12 Jan 2009 01:34:30 GMT

    I just revisited an approach I used to use for *.dev resolving to 127.0.0.1 and created some instructions. This works for OSX using dnsmasq:

    http://inspirix.wikispaces.com/dnsmasq-on-OSX

    Hope this is useful.

    Joe Noon

  2. Avatar
    Nathan de Vries Mon, 12 Jan 2009 03:34:58 GMT

    Nice article. Just thought I’d share how I prefer to do the subdomain downcasing, for those who are interested. Rather than doing it in a callback, I prefer to override the setter method for the attribute that requires custom behavior like so:

    
      class Account < ActiveRecord::Base
        def subdomain=(subdomain)
          @subdomain = subdomain.downcase unless subdomain.blank?
        end
      end
    

    I find this makes it slightly easier to test as well.

    Also, you mentioned editing /etc/hosts on UNIX operating systems to add each of the subdomains. For OS X (Leopard, specifically), doing that isn’t recommended. Instead, you should use the “dscl” command. A guy by the name of Bodaniel Jeanes wrote a nice little gem called ghost” which allows you to add a new host like this:

    ghost add lollipops.purplecowapp.dev

    Hope that helps. Thanks again for the article!

  3. Avatar
    Peter De Berdt Mon, 12 Jan 2009 14:18:35 GMT

    Imho, there’s an even better option: use OS X’s built-in nameserver to handle local domains and subdomains.

    We are usually working on several applications at the same time, each with their own authenticated sessions, some with subdomains as the account key.

    Using the instructions at http://woss.name/2006/11/13/setting-up-local-name-server-on-mac-os-x/ we now have a hasslefree way of handling these circumstances (I use appname.dev.rails for none subdomain keyed apps and something.sub.rails for those that have it, but that’s something you can decide for yourself).

    Only pitfall: when you have no internet connection, it will stop working. It needs an internet connection to work properly.

  4. Avatar
    Dave Mon, 12 Jan 2009 20:36:41 GMT
  5. Avatar
    Jonathan Sutherland Tue, 13 Jan 2009 04:01:56 GMT

    What approach would you take to prevent subdomain-addressed requests from displaying your promotional content?

    e.g. Prevent requests to hacker.purplecowapp.com/signup from displaying your promotional “signup” content.

    I’ll throw a few ideas out there (not necessarily applicable to your app):

    • Separate promotional and product content into two apps
    • Use a before filter to prevent promotional content from being displayed if the request has a subdomain
    • Use namespaces to separate promotional and product content
    • Conditional routes (don’t think this is possible in Rails yet…)

    Any others?

    Cheers

  6. Avatar
    Sean Huber Wed, 14 Jan 2009 00:12:31 GMT

    Cool article

    @jonathan – I believe http://github.com/mbleigh/subdomain-fu integrates with Rails’ routing internals to provide the ability to specify routes based subdomains

    http://github.com/shuber/subdomain_account works nicely when combined with subdomain-fu

  7. Avatar
    Millisami Thu, 15 Jan 2009 20:38:43 GMT

    Hi, nice article! I followed the whole instruction and its working. I also included restful_authentication and its functioning too. Now, how can I associate between the Account Model that we’ve from this article and the User Model that we got from the restful_authentication plugin?? Can anyone please show me some direction?

  8. Avatar
    elioncho Fri, 16 Jan 2009 17:27:54 GMT

    Hello,

    Is there a way to log into one’s account (subdomain) from the domain.

    Example:

    Logging from www.foo.com/session/new,

    will redirect me to,

    elioncho.foo.com

    Thanks for a great post,

  9. Avatar
    elioncho Fri, 16 Jan 2009 17:27:59 GMT

    Hello,

    Is there a way to log into one’s account (subdomain) from the domain.

    Example:

    Logging from www.foo.com/session/new,

    will redirect me to,

    elioncho.foo.com

    Thanks for a great post,

  10. Avatar
    cam Fri, 16 Jan 2009 21:14:52 GMT

    In your Gist what do you mean about your TODO for handling www requests?

    Thanks. Cool post!

  11. Avatar
    Alex Sun, 18 Jan 2009 13:19:03 GMT

    Just a quick note, you need to change the account_subdomain method in the Gist if you are developing on OS X. It doesn’t take the request.subdomains.first, but needs request.subdomains(0).first.

    You could update the method to the something like the example below if you are experiencing any issues.

    def account_subdomain
    if ENV["RAILS_ENV"] == 'development'
      request.subdomains(0).first || '' #We do this for OS X in Development mode
    else
      request.subdomains.first || ''
    end
    end
  12. Avatar
    Vinay Tue, 20 Jan 2009 06:23:29 GMT

    Hey Robbie,

    Nice article! Really think it deserves a more explanatory title like “37 signals style subdomains in Rails” cos there’re just too many “subdomains in rails” kind of articles out there. Really explanatory and a lot of useful code! I was wondering how you handle routing exceptions. Like if the user types in “www.purplecowapp.com/projects” in the browser url field. Meaning, frm the promo site, he tries to access the actual app’s actions, without a valid subdomain. Also, could you put up some code from the routes.rb file and explain how you require the presence of a subdomain for that url to be valid? For eg., in subdomain-fu, you gotta specify :conditions => {:subdomain => /.+/} for every named_route/resource that u require to have a subdomain (i think). So could you explain how you have the routes.rb file setup?

  13. Avatar
    Vinay Tue, 20 Jan 2009 06:24:48 GMT

    Whoops! Sorry about the spelling error with your name Robby :)

  14. Avatar
    Arik Jones Tue, 27 Jan 2009 09:49:24 GMT

    Thanks so much for this. This helped me to understand how subdomains work. Now if only I can scope user session to an account. Shouldn’t be too difficult.

    P.S, I’m a designer trying to be a rails developer.

  15. Avatar
    Whit Kemmey Wed, 28 Jan 2009 06:28:58 GMT

    I am trying to decide which is better:

    1) account.example.com goes to a overview page of my application (map.root :controller => ‘overview’, :action => ‘index’), forcing example.com to be redirected to example.com/home

    2) example.com (without subdomain) is the “real” root of my app (map.root :controller => ‘site’, :action => ‘home’), forcing account.example.com to be redirected to account.example.com/overview

    I am thinking number one so that paying customers have the neatest url for their homepage.

    Anybody have any ideas about the best way to handle this?

  16. Avatar
    mahesh Wed, 11 Feb 2009 10:01:38 GMT

    i have doubts few doubts with “account_location” plugin.

    1). does account_location plugin works in development environment.

    2). does account_location plugin work in the windows system, as i have all posts are explained in case of unix systems.

  17. Avatar
    Arik Jones Thu, 05 Mar 2009 05:02:01 GMT

    Handling “www” and blank subdomains.

    def default_account_subdomain
      if ["www", ""].include?(account_subdomain)
        account_subdomain
      end
    end
  18. Avatar
    forex trading tools Fri, 15 May 2009 11:23:34 GMT

    In my opinion, you should use the last subdomain, “request.subdomains.last” instead of the first. For our online stores at Storenvy, I’ve seen a lot of users try “www.subdomain.storenvy.com”. I’ve even seen store owners publish their links in that format. So, I figured I should just make it work for people.Any reason why one shouldn’t do this? I noticed Basecamp redirects if you put “www” at the start of the URL . But is that necessarily better?

Share your thoughts... (really...I want to hear them)

Comments