SubdomainFu: A New Way To Tame The Subdomain

An extremely common practice for Rails applications is to provide keyed access through subdomains (i.e. http://someaccount.awesomeapp.com/). However, there has never been a real unified convention for handling this functionality. DHH’s Account Location works for some circumstances but is more tailored for a Basecamp domain model (i.e. the app is on a separate domain from all other functionality, so you can always expect a subdomain) than the more common usage of one domain only.

SubdomainFu aims to provide a simple, generic toolset for dealing with subdomains in Rails applications. Rather than tie the functionality to something specific like an account, SubdomainFu simply provides a foundation upon which any subdomain-keyed system can easily be built.

Usage Fu

SubdomainFu works by riding on top of the URL Rewriting engine provided with Rails. This way you can use it anywhere you normally generate URLs: through url_for, in named routes, and in resources-based routes. There’s a small amount of configuration that is needed to get you running (though the defaults should work for most).

To set it up, you can modify any of these settings (the defaults are shown):

# in environment.rb

# These are the sizes of the domain (i.e. 0 for localhost, 1 for something.com)
# for each of your environments
SubdomainFu.tld_sizes = { :development => 0,
                          :test => 0,
                          :production => 1 }

# These are the subdomains that will be equivalent to no subdomain
SubdomainFu.mirrors = ["www"]

# This is the "preferred mirror" if you would rather show this subdomain
# in the URL than no subdomain at all.
SubdomainFu.preferred_mirror = "www"

Now when you’re in your application, you will have access to two useful features: a current_subdomain method and the URL Rewriting helpers. The current_subdomain method will give you the current subdomain or return nil if there is no subdomain or the current subdomain is a mirror:

# http://some_subdomain.myapp.com/
current_subdomain # => "some_subdomain" 

# http://www.myapp.com/ or http://myapp.com/
current_subdomain # => nil

# http://some.subdomain.myapp.com
current_subdomain # => "some.subdomain"

The URL rewriting features of SubdomainFu come through a :subdomain option passed to any URL generating method. Here are some examples (in these examples, the current page is considered to be ‘http://intridea.com/’):

url_for(:controller => "my_controller", 
  :action => "my_action", 
  :subdomain => "awesome") # => http://awesome.intridea.com/my_controller/my_action

users_url(:subdomain => false)  # => http://intridea.com/users

# The full URL will be generated if the subdomain is not the same as the
# current subdomain, regardless of whether _path or _url is used.
users_path(:subdomain => "fun") # => http://fun.intridea.com/users
users_path(:subdomain => false) # => /users

While this is just a simple set of tools, it can allow the easy creation of powerful subdomain-using tools. Note that the easiest way to locally test multiple subdomains on your app is to edit /etc/hosts and add subdomains like so:

127.0.0.1    localhost subdomain1.localhost subdomain2.localhost www.localhost

Adding an entry for each subdomain you want to use locally. Then you need to flush your local DNS cache to make sure your changes are picked up:

sudo dscacheutil -flushcache

Installation

SubdomainFu is available both as a traditional plugin and as a GemPlugin for Rails 2.1 and later. For a traditional plugin, install like so:

script/plugin install git://github.com/mbleigh/subdomain-fu.git

For a GemPlugin, add this dependency to your environment.rb:

config.gem 'mbleigh-subdomain-fu', :source => "http://gems.github.com/", :lib => "subdomain-fu"

Implementing A Simple Account Key System

Let’s take this functionality and implement a simple account-key system based off of the subdomain. We’ll start with some controller code (assuming that we have an Account model with a ‘subdomain’ field):

class ApplicationController < ActionController::Base
  protected

  # Will either fetch the current account or return nil if none is found
  def current_account
    @account ||= Account.find_by_subdomain(current_subdomain)
  end
  # Make this method visible to views as well
  helper_method :current_account

  # This is a before_filter we'll use in other controllers
  def account_required
    unless current_account
      flash[:error] = "Could not find the account '#{current_subdomain}'" 
      redirect_to :controller => "site", :action => "home", :subdomain => false
    end
  end
end

That’s really all we need for a basic setup, now let’s say we have a ProjectsController that you must specify an account to access:

class ProjectsController < ApplicationController
  # Redirect users away if no subdomain is specified
  before_filter :account_required
end

There’s lots more you can do with the plugin, but this is a simple use case that everyone can relate to.

Resources and Plans

A feature that I hoped would make it to the first release of SubdomainFu but is now a planned feature is subdomain-aware routing so that you can add conditional subdomain routes to your routes.rb file. Keep an eye out for more on that in the future.

In the meantime, the project will live at its home on Acts As Community for intermittent updates, is available on GitHub as always, and bugs/feature requests may be passed on through the Lighthouse.

Share:

26 Responses to “SubdomainFu: A New Way To Tame The Subdomain”

  1. Mark Holton

    Very cool. Greatly appreciate the info! (an fyi... your font is all alias-y on FF3)
  2. Raecpp

    nice,very useful
  3. Zack

    Looks very nice - I've always used the request_routing plugin for this... conditional subdomain routes will be a great addition to this plugin.
  4. John

    Nice plugin ... one very minor annoyance though is it seems to pollute RSpec output with 'We're changing the host' ... might be worth moving those messages into the Rails logger.
  5. Greg

    Thanks for this. It is great. One thing that I noticed. If you have a redirect_to in your controller and include the :subdomain key the flash gets cleared. If you leave that out then the flash stays around. Any ideas why that would be?
  6. Michael Bleigh

    @John: oops! I always forget those stray puts. Removed and pushed to GitHub, try the latest version! @Greg: interesting, I'm guessing the flash problem has to do with changing the host. I've made a ticket for it in the Lighthouse, feel free to follow the progress there! http://mbleigh.lighthouseapp.com/projects/13148/tickets/2-flash-is-cleared-upon-changing-subdomains
  7. Stpehen Wooten

    I ran my test suite to puts the current_subdomain as a before_filter in application.rb and it kept outputting 'test'. I don't understand where this 'test' is coming from...Any ideas?
  8. John C.

    I just set up the plugin today and so far everything is working quite well; this is just the sort of thing that I was looking for. I noticed too that the routing_extensions.rb file is now present, although it's not currently being included. I included the file and set up some conditionals in the routes any everything behaved as expected. Granted, I didn't do anything super complex - really I just set up three different landing pages based on subdomains - but that did work as expected. I'm curious: do you have a list of objectives that will need to be met before this portion of the plugin is deemed production ready, and if so, could you share that list somewhere?
  9. Michael Bleigh

    @Stephen Wooten: I believe that RSpec sets the default host to "test.host" which conflicts with the default test tld_size of 0. I may change the default in the future, but for now you can just add "SubdomainFu.tld_size = 1" to your test.rb in an after_initialize block. @John C: The holdoff on production-ready routing is writing a complete set of specs for it and getting it to work with RESTful routes. The timeline for that should be in the near future (I need it for some work I'm doing), but I don't have a specific date in mind yet.
  10. W. Andrew Loe III

    How can I instruct Subdomain-Fu to NOT rewrite all links to the preferred host in the absence of a :subdomain option? If I come to subdomain.example.com all of my links that do not expressly have :subdomain defined in them are being rewritten to www.example.com I want it to keep using relative urls and only change if I define a subdomain that is different from the current one.
  11. Michael Bleigh

    @W. Andrew Loe: You've actually discovered a bug in SubdomainFu! I will be taking a look at this very soon, you can track the progress here: http://mbleigh.lighthouseapp.com/projects/13148-subdomain-fu/tickets/3-subdomain-is-stripped-unless-specified-when-preferred-domain-is-set
  12. Dylan

    Thanks a ton for releasing this. In addition to W. Andrew Loe III's bug, I am noticing that, while users_path(:subdomain => "fun") # => http://fun.intridea.com/users works as expected, user_path(user, :subdomain => project.permalink) doesn't. It should be something like # => http://fun.intridea.com/users/1 but instead I am getting # => http://intridea.com/users/1?subdomain=fun Let me know if there is something I am doing wrong. Thanks!
  13. Andrew Watkins

    Some some routes are optimized when they're generated so routing can remain fast. Any options that the route doesn't expect when it hits this generated code gets added on the end of hte query string. Here is some sample code from an inhouse implementation of subdomainfu. It has not been tested against rails 2.1, but works with rails 2.0.2 (and actually might now work since we can't seem to find request.domain anymore): module ActionController module Routing module Optimisation class Subdomains < PositionalArguments def guard_condition "defined?(request) && request && args.size == #{route.segment_keys.size + 1} && args.last.has_key?(:subdomain)" end # replace request.host_with_port with our subdomain, the domain, and the port def generation_code super.sub('#{request.host_with_port}', '#{args.last[:subdomain]}.#{request.domain(1)}#{(\':\' + request.port.to_s) if request.port != request.standard_port}') end def applicable? super && route.segment_keys.size > 0 end end class DefaultPositionalArguments < PositionalArguments # replace request.host_with_port with the default subdomain, www, the domain, and the port def generation_code super.sub('#{request.host_with_port}', 'www.#{request.domain(1)}#{(\':\' + request.port.to_s) if request.port != request.standard_port}') end end OPTIMISERS = [DefaultPositionalArguments, Subdomains, PositionalArgumentsWithAdditionalParams] end end end
  14. Dave Spurr

    I'm having a terrible time with subdomain-fu and Rspec. I just added an observer that sends the emails when a user is created etc. If run all tests it works fine, however if I just run the tests for the user model I get the following error: The error occurred while evaluating nil.split /vendor/plugins/subdomain-fu/lib/subdomain-fu.rb:46:in `host_without_subdomain' Is there anyway to consistently set the domain (with subdomain) across all of my tests using RSpec?
  15. Michael Bleigh

    @Dave Spurr: you need to set the default_url_options in your spec_helper. Try this: default_url_options[:host] = "localhost" or some other test host that suits you better.
  16. Dave Spurr

    @Michael Bleigh Thanks for that, I'm also finding I have to set the @request.host to my designated test host in the before for all of my controller specs, do you know if it's possible to set this somewhere in the spec_helper?
  17. Sebastian

    Very usefull article!
  18. Rick Sandhu

    Can you please share your thoughts on caching subdomain based applications using memcache? It's hard to apply available reference on caching to subdomain based applications.
  19. Andy Stewart

    This works well for me -- thanks! I find it more intuitive and more useful than the Account Location plugin.
  20. Other Dylan

    Awesome plugin, but I'm running into the same problem as the other Dylan above is. The hack that someone mentioned in the relating ticket is not working either.
  21. Dylan (First Dylan)

    Hey, Other Dylan, give this a shot. http://mbleigh.lighthouseapp.com/projects/13148/tickets/8-improper-generated-urls-with-named-routes-for-a-singular-resource#ticket-8-3 Turning off route optimization worked for me.
  22. Dylan (Second Dylan)

    Hey First Dylan :) That hack worked perfectly, but yeah, it is majorly hacky. Will watch that ticket. Thanks !
  23. ssdas

    Hey guys, actually I am completely new in this field, on the learning stage. So i have no clear idea about subdomains. by the way i have read this articles and implement it successfully by adding "127.0.0.1 admin.localhost" to /etc/hosts file. Now i have a domain on the dreamhost server and what should I do if I want to run my application on that domain. Actually my problem is i want to dynamically create sub domains from my application. if any one know about this, then please give details. Thanks, ssdas
  24. Michael Bleigh

    @ssdas - in order to set up dynamic domain names you will need to set up a Wildcard DNS record and have a static IP for your hosting account. If you are on a shared Dreamhost account this will not be possible, but I believe it can be done on a VPS account by adding an extra feature to the account. You will need to manually e-mail Dreamhost to set up the wildcard if your domain is hosted through them.
  25. Maarten

    After using account_location for a while I discovered this great plugin. You can even use things like url_for(:subdomain => user.subdomain) in ActionMailer!
  26. babs.strange

    Great plugin, but is it possibile to have all links still linking to example.com when on subdomain.example.com? I don't really feel like adding {:subdomain => false} to every single one of them....

Sorry, comments are closed for this article.