Skip to content
Daniel Kehoe edited this page Nov 14, 2011 · 15 revisions

Rails 3.1 with Subdomains and Devise

This is a detailed Rails tutorial showing how to create an example Rails 3.1 application with subdomains and authentication using Devise.

What Is Implemented

The example app implements a common use of subdomains, often called “Basecamp-style subdomains in Rails.” Visitors to the main site create a user account which is hosted at a subdomain that matches their user name. Each user has only one subdomain and when they log in, all their activity is confined to their subdomain.

What Is Not Implemented

Another common use of subdomains can be called “blog-style subdomains.” This approach is familiar to users of sites such as wordpress.com where each user can create multiple blogs and each is hosted on its own subdomain. For example, a user with the email address “[email protected]@” can own more than one blog, for example, “puppyphotos.wordpress.com” and “kittenphotos.wordpress.com”. The “blog-style subdomains” approach is not implemented in this example application.

Based on the Rails3-Mongoid-Devise Example App

This app extends the Rails 3 + Mongoid + Devise example app. You can see a tutorial for the Rails 3 + Mongoid + Devise. Mongoid gives access to a MongoDB datastore for quick development without schemas or migrations. Devise gives you ready-made authentication.

Similar Examples and Tutorials

See a list of additional Rails examples, tutorials, and starter apps.

Follow on Twitter Follow on Twitter

Follow the project on Twitter: @rails_apps. Tweet some praise if you like what you’ve found.

Tutorial Tutorial

This tutorial documents each step that you must follow to create this application. Every step is documented concisely, so a complete beginner can create this application without any additional knowledge. However, no explanation is offered for any of the steps, so if you are a beginner, you’re advised to look for an introduction to Rails elsewhere. See a list of top resources for learning Rails.

Before You Start

If you follow this tutorial closely, you’ll have a working application that closely matches the example app in this GitHub repository. The example app is your reference implementation. If you find problems with the app you build from this tutorial, download the example app (in Git speak, clone it) and use a file compare tool to identify differences that may be causing errors. On a Mac, good file compare tools are FileMerge, DiffMerge, Kaleidoscope, or Ian Baird’s Changes.

If you clone and install the example app and find problems or wish to suggest improvements, please create a GitHub issue.

To improve this tutorial, please edit this wiki page.

Creating the Application

Option One

Cut and paste the code.

To create the application, you can cut and paste the code from the tutorial into your own files. It’s a bit tedious and error-prone but you’ll have a good opportunity to examine the code closely.

Option Two

Use the ready-made application template to generate the code.

You can use an application template to generate a new Rails app with code that closely matches the tutorial. You’ll find an application template for this tutorial in the Rails Application Templates repository.

Use the command:

$ rails new myapp -m https://github.com/RailsApps/rails3-application-templates/raw/master/rails3-subdomains-template.rb -T -O

Use the -T -O flags to skip Test::Unit files and Active Record files.

This creates a new Rails app (with the name myapp) on your computer. It includes everything in the example app. You can read through the tutorial with the code already on your computer.

Option Three

Use the rails_apps_composer gem to create a reusuable application template.

This is optimal if you are creating a “starter app” based on this example app but wish to customize the code for your own preferences.

Each step in this tutorial has a corresponding application template recipe from the Rails Apps Composer recipes repository. You can create your own application template using the template recipes. To do so, download the Rails Apps Composer project, customize recipes as needed, and follow the instructions to create a reusable application template file.

Assumptions

Before beginning this tutorial, you need to install

  • The Ruby language (version 1.9.2)
  • Rails 3.1.1
  • A working installation of MongoDB

Check that appropriate versions of Ruby and Rails are installed in your development environment:
$ ruby -v
$ rails -v

See Installing Rails 3.1 and Managing Rails Versions and Gems for detailed instructions and advice.

Installing MongoDB

If you don’t have MongoDB installed on your computer, you’ll need to install it and set it up to be always running on your computer (run at launch). On Mac OS X, the easiest way to install MongoDB is to install Homebrew and then run the following:

brew install mongodb

Homebrew will provide post-installation instructions to get MongoDB running. The last line of the installation output shows you the MongoDB install location (for example, /usr/local/Cellar/mongodb/1.8.0-x86_64). You’ll find the MongoDB configuration file there. After an installation using Homebrew, the default data directory will be /usr/local/var/mongodb.

Create the Rails Application

You’ll start by generating the Rails 3 + Mongoid + Devise example app using an application template.

Use the command:

$ rails new rails3-subdomains -m https://github.com/RailsApps/rails3-application-templates/raw/master/rails3-mongoid-devise-template.rb -T -O

Use the -T -O flags to skip Test::Unit files and Active Record files.

This creates a new Rails app (named rails3-subdomains) on your computer. For this tutorial, the name is “rails3-subdomains.” You can use a different name if you wish.

The application generator template will ask you for your preferences. For this tutorial, choose the following preferences:

Would you like to use jQuery? Yes.
Would you like to use jQuery UI? No.
Would you like to use Haml instead of ERB? Yes.
Would you like to use RSpec instead of TestUnit? Yes.
Would you like to use factory_girl for test fixtures with RSpec? Yes.
Would you like to use Cucumber for your BDD? Yes.
Would you like to use Guard to automate your workflow? No.
Would you like to enable the LiveReload guard? No.
Would you like to use Mongoid to connect to a MongoDB database? Yes.
Would you like to use Devise for authentication? Yes.
Which front-end framework would you like for HTML5 and CSS3? Zurb Foundation
Would you like to set a robots.txt file to ban spiders? Yes.
Would you like to use ‘rails-footnotes’ during development? No.

After you create the application, switch to its folder to continue work directly in the application:

$ cd rails3-subdomains

Please Remember: Edit the README

If you’re open sourcing the app on GitHub, please edit the README file to add a description of the app and your contact info. Changing the README is important if you’re using a clone of the example app. I’ve been mistaken (and contacted) as the author of apps that are copied from my example.

Set Up Source Control (Git)

If you’re creating an app for deployment into production, you’ll want to set up a source control repository at this point. If you are building a throw-away app for your own education, you may skip this step.

See instructions for Using Git with Rails.

Set Up Gems

About Required Gems

The application uses the following gems:

jQuery

Rails 3.1 uses jQuery by default so no additional effort is required.

Haml

In this example, we’ll use Haml instead of the default “ERB” Rails template engine. The Rails 3 + Mongoid + Devise example app sets up Haml. You can see details about adding Haml to Rails.

RSpec

The Rails 3 + Mongoid + Devise example app uses RSpec for unit testing. Run rake -T to check that rake tasks for RSpec are available. You should be able to run rake spec to run all specs provided with the example app.

Cucumber

The Rails 3 + Mongoid + Devise example app sets up Cucumber for specifications and acceptance testing. You should be able to run rake cucumber (or more simply, cucumber) to run the Cucumber scenarios and steps provided with the example app.

Mongoid

The Rails 3 + Mongoid + Devise example app uses Mongoid for access to a a MongoDB datastore. The datastore is initialized with a default user in the the file db/seeds.rb.

If you need to, you can run $ rake db:reset to drop and then recreate the database using your seeds.rb file.

Set up Your Gemfile

The Rails 3 + Mongoid + Devise application template sets up your gemfile.

See an example Rails 3.1.1 Gemfile.

See Managing Rails Versions and Gems for advice and details. It’s a good idea to create a new gemset using rvm, the Ruby Version Manager.

Install the Required Gems

Install the required gems on your computer:

$ bundle install

You can check which gems are installed on your computer with:

$ gem list --local

Keep in mind that you have installed these gems locally. When you deploy the app to another server, the same gems (and versions) must be available.

Test the App

Seed the Database

Add the default user to the MongoDB database by running the command:

$ rake db:seed

Test the App

You can check that your app runs properly by entering the command

$ rails server

To see your application in action, open a browser window and navigate to http://localhost:3000/. You should see the default user listed on the home page. When you click on the user’s name, you should be required to log in before seeing the user’s detail page.

To sign in as the default user, (unless you’ve changed it) use

You should delete or change the pre-configured logins before you deploy your application.

Stop the server with Control-C.

One Subdomain Per User

Use Case

Each user will have access to their own profile page with a unique URL that includes a custom subdomain. When a user creates a new account, they will be given an opportunity to specify a user ID that will be used as the subdomain. For example, when I sign up for an account, I might specify my User ID as “kehoe” and my profile page will be available at kehoe.example.com.

User Name as a Subdomain

The rails3-mongoid-devise example app has a field in the User model named “name.” We’ll use this for the user-specific subdomain.

If you wish, you could rename the “name” field as “subdomain” or “account” or use another label that better matches the requirements of your application. You could also add fields to the User model such as “firstname” and “lastname.” If you do so, you’ll need to change the forms in the app/views/devise/registrations directory as well as app/models/user.rb. For simplicity, we’ll just use the “name” field to set the subdomain.

In its current form, the User model allows any string to be stored as the user name. To use it as a subdomain, we’ll need to make sure it only contains alphanumeric characters (with an optional underscore). We’ll also limit the name to a maximum of 32 characters. Finally, we don’t want users to choose names such as “www” that will result in URLs such as “http://www.myapp.com”.

Modify app/models/user.rb to add the following validations:

validates_format_of :name, with: /^[a-z0-9_]+$/, message: "must be lowercase alphanumerics only"
validates_length_of :name, maximum: 32, message: "exceeds maximum of 32 characters"
validates_exclusion_of :name, in: ['www', 'mail', 'ftp'], message: "is not available"

We’ll modify the db/seeds.rb file to supply an appropriate user name when we initialize the datastore:

# user = User.create! :name => 'First User', :email => '[email protected]', :password => 'please', :password_confirmation => 'please'
user = User.create! :name => 'myname', :email => '[email protected]', :password => 'please', :password_confirmation => 'please'

User’s Profile Page

Use Case

We’ll display a profile page for a user when anyone enters a URL with a subdomain that matches an existing user. For example, visiting kehoe.example.com will display kehoe’s profile page.

Controller and Views for the Profile Page

Create a controller for profiles:

$ rails generate controller Profiles show

Before we display a profile, we’ll try to find the user with a name that matches the subdomain attribute of the request object. If the user is not found, we’ll raise an exception and display an error message.

app/controllers/profiles_controller.rb

  class ProfilesController < ApplicationController
    def show
      @user = User.first(conditions: { name: request.subdomain }) || not_found
    end

    def not_found
      raise ActionController::RoutingError.new('User Not Found')
    end

  end

Next, modify the view to display a user’s profile. Add the following code:

app/views/profile/show.html.erb

%h1 Profile
%h4= @user.name
%h4= @user.email

Implement Routing for Subdomains

Rails 3.0 introduced support for routing constrained by subdomains.

A subdomain can be specified explicitly, like this:

match '/' => 'home#index', :constraints => { :subdomain => 'www' } 

Or a set of subdomains can be matched using a regular expression:

match '/' => 'profiles#show', :constraints => { :subdomain => /.+/ } 

Finally, for greatest flexibility, router constraints can also take objects, allowing custom code.

Create a class like this:

lib/subdomain.rb

class Subdomain
  def self.matches?(request)
    case request.subdomain
    when 'www', '', nil
      false
    else
      true
    end
  end
end

This class allows use of a route when a subdomain is present in the request object. If the subdomain is “www,” the class will respond as if a subdomain is not present.

Make sure the class is autoloaded when the application starts. You can require 'subdomain' at the top of the config/routes.rb file. Or you can modify the file config.application.rb (recommended):

# config.autoload_paths += %W(#{config.root}/extras)
config.autoload_paths += %W(#{config.root}/lib)

Use this class when you create routes in the file config/routes.rb:

devise_for :users
resources :users, :only => :show
constraints(Subdomain) do
  match '/' => 'profiles#show'
end
root :to => "home#index"

A match from a “/” URL (such as http://myname.myapp.com) will route to the show action of the Profiles controller only when a subdomain is present. If a subdomain is not present (or is “www”), a route with less priority will be applied (in this case, a route to the index action of the Home controller).

Be sure to comment out (or remove) the route that was added by the Rails generator when we created the controller:

#get "profiles/show"

Home Page

The rails3-mongoid-devise example app provides a home page that lists all registered users. We’ll modify the home page to add a link to each user’s profile page, using a URL with a custom subdomain.

app/views/home/index.html.haml

%h4 Home
- @users.each do |user|
  %br/ 
  User: #{link_to user.name, user}
  Profile: #{link_to root_url(:subdomain => user.name), root_url(:subdomain => user.name)}

URL Helpers With Subdomains

Applications that do not use subdomains use routing helpers to generate links that either include the site’s hostname (for example, users_url generates http://mysite.com/users) or links that only contain a relative path (for example, users_path generates /users). To provide navigation between sites hosted on the subdomains and the main site, you must use URL helpers (“users_url”) not path helpers (“users_path”) because path helpers do not include a hostname. Rails 3.1 provides a way to include a subdomain as part of the hostname when generating links.

You can specify a hostname when creating a link, with the syntax:

root_url(:subdomain => @subdomain)

If you need a link to the main site (a URL without a subdomain), you can force the URL helper to drop the subdomain:

root_url(:host => request.domain)

Is there a better way to do this? Open an issue if you have a suggestion.

Test the Application With Subdomains

If you launch the application, it will be running at http://localhost:3000/ or http://0.0.0.0:3000/. However, unless you’ve made some configuration changes to your computer, you won’t be able to resolve an address that uses a subdomain, such as http://foo.localhost:3000/.

Some Options

There are several complex solutions to this problem. You could set up your own domain name server on your localhost and create an A entry to catch all subdomains. You could modify your /etc/hosts file (but it won’t accommodate dynamically created subdomains). You can create a proxy auto-config file and set it up as the proxy in your web browser preferences.

Use lvh.me

There’s a far simpler solution that does not require reconfiguring your computer or web browser preferences. The developer Levi Cook registered a domain, lvh.me (short for: local virtual host me), that resolves to the localhost IP address 127.0.0.1 and supports wildcards (accommodating dynamically created subdomains).

Seed the Datastore

You’ve changed the name of the default user in the the db/seeds.rb file.

Re-initialize the MongoDB datastore by running the command:

$ rake db:seed

Test the App

Start the app by entering the command

$ rails server

To test the application, visit http://lvh.me:3000/. You should see a list of registered users with links to their profile pages.

Try http://myname.lvh.me:3000/. You should see the profile page for the default user “myname.”

Try http://foo.lvh.me:3000/. You should see the “user not found” error message.

Try http://www.lvh.me:3000/. You should be redirected to the application home page at http://lvh.me:3000/.

Optional: Allow Sessions To Be Shared Across Subdomains

The application allows a user to log in to either the main site (http://lvh.me:3000/) or a user site (http://myname.lvh.me:3000/). Logging in creates a user session. In its current form, the application maintains separate and independent user sessions for the main site and each subdomain. That’s because session data for each visitor is managed by browser-based cookies and cookies are not shared across domains (and, by default, not shared across subdomains). Not only is the user login not maintained between the main site and subdomains, but flash messages (used to communicate between actions) are lost because they are stored in sessions.

For the example application, we want a visitor to sign up on the main site but only log in on their subdomain-hosted site. As implemented, we don’t want sessions to be shared across subdomains.

However, your requirements may differ. If you wish to maintain sessions between the main site and subdomain-hosted sites, modify the configuration file to add the parameter :domain => :all:

config/initializers/session_store.rb

Rails3Subdomains::Application.config.session_store :cookie_store, :key => '_rails3-subdomains_session', :domain => :all

Navigation Links

We want a visitor to sign up on the main site but only log in on their subdomain-hosted site.

We’ll change the navigation links to show “Sign up” on the main site and “Login” on the subdomain sites.

app/views/shared/_navigation.html.haml

%li
  = link_to 'Main', root_url(:host => request.domain)
- unless request.subdomain.present? && request.subdomain != "www"
  - if user_signed_in?
    %li
      = link_to('Edit account', edit_user_registration_path)
  - else
    %li
      = link_to('Sign up', new_user_registration_path)
- else
  - if user_signed_in?
    %li
      = link_to('Logout', destroy_user_session_path, :method=>'delete')
  - else
    %li
      = link_to('Login', new_user_session_path)

Deploy to Heroku

For your convenience, here are instructions for deploying your app to Heroku. Heroku provides low cost, easily configured Rails application hosting.

Conclusion

This concludes the tutorial for creating a Rails 3 web application that uses subdomains and provides user management and authentication using Devise.

Credits

Daniel Kehoe (http://danielkehoe.com/) implemented the application and wrote the tutorial.

Was this useful to you? Follow me on Twitter:
rails_apps
and tweet some praise. I’d love to know you were helped out by the tutorial.

Any issues? Please create an issue on GitHub.

Clone this wiki locally