Subdomains (or second level domains) are still a nice way to present separate interfaces to a single web app — commonly to give a certain kind of customers the feel-n-touch of a dedicated app install.
Here are most of the related aspects based on a particular Rails3 project:
some model
Group
, representing a customer, or rather a group of users, has a string columndomain
(migration not shown, but don’t forget to index by that column)in
app/models/group.rb
:has_many :users before_validation :downcase_domain, :if => :domain_changed? validates :domain, presence: true, uniqueness: true, length: {maximum: 255}, format: /^[0-9a-z-]+$/ def host "#{domain}.#{ROOT_DOMAIN}" end protected def downcase_domain self.domain = domain.to_s.downcase end
under assumption that you’re using
devise
and your (group’s) user identity model isUser
(updated for everchangingdevise
2.0.4)in
app/models/user.rb
:belongs_to :group devise :database_authenticatable, ..., :request_keys => [:app_subdomain] def self.find_for_authentication(conditions={}) group = Group.find_by_domain(conditions.delete(:app_subdomain)) return nil unless group.present? conditions[:group_id] = group.id super end
you’ll have some admin interface where the
groups
are managed (e.g.active_admin
, not recommended)in
app/admin/groups.rb
:link_to group.domain, root_url(host: group.host)
you will certainly want to do something specific in
config/routes.rb
you’ll want some handy helper method, to know which guvnor you’re serving
in
app/controllers/application_controller.rb
:helper_method :current_group def current_group return @current_group if defined? @current_group @current_group = request.app_subdomain && Group.find_by_domain(request.app_subdomain) end
you’ll need an initializer of some sort to set a constant and monkey-patch the request class, so…
in
config/initializers/subdomain.rb
:ROOT_DOMAIN ||= ENV['ROOT_DOMAIN'] or raise "ROOT_DOMAIN must be set" # (): paranoid monkey patching :() class ActionDispatch::Request def app_subdomain return @app_subdomain if defined? @app_subdomain @@app_hostname_regex ||= /^(?:([0-9a-z-]+).)?#{Regexp.escape(ROOT_DOMAIN)}$/ raise 'Wrong domain' unless host.downcase =~ @@app_hostname_regex @app_subdomain = $1 end end
then, for your production (and staging) environment on, say, heroku, you’ll have to setup your lovely app domain name (with wildcard subdomains) and set the environment variable
ROOT_DOMAIN
to itfor test environment, which is also good for the handy circleci
in
config/environments/test.rb
:ROOT_DOMAIN = 'lvh.me' # yes, it's a magic domain for 127.0.0.1 //smackaho.st RIP
for other environment cases, be sure to set either
ROOT_DOMAIN
orENV['ROOT_DOMAIN']
as appropriateif you use factories (and girls,
factory_girl
)in
spec/factories.rb
:factory :group do sequence(:domain) {|n| "dom-#{n}"} # or better still, use `forgery` with some smart randomness ... end
if you use
capybara
(2.0.0 at least, recommended) andrspec
(rspec-rails
2.12.0 at least)in
spec/spec_helper.rb
: (inSpork.prefork
block if you usespork
, recommended)Capybara.always_include_port = true # unless you `visit` external sites in your feature specs
and then in some
spec/features/..._spec.rb
:visit("http://some_domain.#{ROOT_DOMAIN}/some_path") # or visit(some_url host: @group.host) # if you're playing dirty, using pre-fabricated data and route helpers, recommended
in some
spec/controllers/..._spec.rb
you’ll have to include something like this:before(:each) do request.host = @group.host end
don’t forget the specs for domains in
spec/models/group_spec.rb
and other relevant places
May your sub-domains be obedient to their master.