Subdomains are quite useful in a number of different scenarios. For example, let’s say you are building a multi-user application. Each user gets their own space. Being able to give each user a unique subdomain allows you to give users their own personal space.

For security point of view, this is also very beneficial because all subdomain has their own DB schema.So, if you do not have access rights then you can not able to reach other domain.

Click here to share this article on LinkedIn »

Now let’s implement the SubDomain functionality.I used PostgreSQL as a database.

Below are the following steps:

Step1: Add the below line in your Gemfile.

to achieve the subdomain functionality, I’m going to use the ‘apartment’ gem. for more details, you can check the below URL.

https://github.com/influitive/apartment

gem 'apartment' gem 'devise', '~> 4.2' gem 'devise_invitable', '~> 1.7.1'

For devise gem implementation, please refer the below article.

http://qiita.com/alokrawat050/items/5267e6ab0e274ad1188a

Step2: Create some useful controller,model, let’s say account.

rails g controller account new rails g controller welcome index rails g model account new

write some code in controller,

→accounts_controller.rb

class AccountsController < ApplicationController

skip_before_filter :authenticate_user!, only: [:new, :create]

def new

@account = Account.new

@account.build_owner

#store plan id in session

session[:plan_id] = params[:plan_id]

end



def create

@account = Account.new(accounts_params)

@account.assign_attributes(:plan_id => session[:plan_id])

if @account.valid?

Apartment::Tenant.create(@account.subdomain_name)

Apartment::Tenant.switch!(@account.subdomain_name)

if @account.save

session[:plan_id] = nil

redirect_to new_user_session_url(subdomain: "#{@account.subdomain_name}.demo-alokrawat050")

else

render action: 'new'

end

else

render action: 'new'

end

end



private

def accounts_params

params.require(:account).permit(:subdomain_name, owner_attributes: [:username, :is_admin, :email, :password, :password_confirmation, :password_updated_at])

end

end

→welcome_controller.rb

class WelcomeController < ApplicationController

skip_before_filter :authenticate_user!, only: [:index, :find_team, :find_team_index]

def index

end



def find_team

if params[:team_name].present? && !params[:team_name].nil? && !params[:team_name].blank?

Apartment::Tenant.switch!('public')

if search_team(params[:team_name])

Apartment::Tenant.switch!(params[:team_name])

redirect_to new_user_session_url(subdomain: "#{params[:team_name]}.demo-alokrawat050")

else

flash[:alert] = "Team Not Found."

redirect_to root_path

end

else

flash[:alert] = "Please Enter the Team Name."

redirect_to root_path

end

end



def find_team_index

end



protected

def search_team(team_name)

@account ||= Account.find_by(subdomain_name: team_name)

end

end

application_controller.rb file look alike,

class ApplicationController < ActionController::Base

# Prevent CSRF attacks by raising an exception.

# For APIs, you may want to use :null_session instead.

protect_from_forgery with: :exception



before_filter :load_schema, :authenticate_user!, :set_mailer_host

before_filter :configure_permitted_parameters, if: :devise_controller?



protected

def configure_permitted_parameters

devise_parameter_sanitizer.permit(:accept_invitation){|u|

u.permit(:username, :password, :password_confirmation, :invitation_token, :password_updated_at)

}

end



private

def load_schema

Apartment::Tenant.switch!('public')

return unless get_subdomain_acc.present?



if current_account

Apartment::Tenant.switch!(current_account.subdomain_name)

else

redirect_to root_url(subdomain: false)

end

end



def current_account

@current_account ||= Account.find_by(subdomain_name: get_subdomain_acc)

end



helper_method :current_account



def set_mailer_host

subdomain = current_account ? "#{current_account.subdomain_name}." : ""

if Rails.env == "production"

ActionMailer::Base.default_url_options[:host] = "#{subdomain}<your production name>.com"

elsif Rails.env == "staging"

ActionMailer::Base.default_url_options[:host] = "#{subdomain}<your staging name>.com"

else

ActionMailer::Base.default_url_options[:host] = "#{subdomain}demo-alokrawat050.c9users.io"

end

end



def get_subdomain_acc

if Rails.env == "production"

return request.subdomain

elsif Rails.env == "staging"

return request.subdomain

else

return request.subdomain.gsub!(".demo-alokrawat050","")

end

end



def after_sign_out_path_for(resource_or_scope)

new_user_session_path

end



def after_invite_path_for(resource)

#invite_users_path

root_path

end

end

Step3: Do some work in a helper method.

in application_helper.rb,

module ApplicationHelper

def get_subdomain

return request.subdomain.gsub!(".demo-alokrawat050","")

end



def link_to_add_fields(name, f, association)

new_object = f.object.send(association).klass.new

id= new_object.object_id

fields=f.fields_for(association, new_object, child_index: id) do | builder |

render(association.to_s.singularize + "_fields", f: builder)

end

link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("

", "")})

end



def check_is_admin_rights?

if current_user.is_admin

true

else

false

end

end

end

create a new helpers, form_helper.rb and layout_helper.rb

module FormHelper

def errors_for(form, field)

content_tag(:p, form.object.errors[field].try(:first), class: 'help-block')

end



def form_group_for(form, field, opts={}, &block)

label = opts.fetch(:label) { true }

has_errors = form.object.errors[field].present?



content_tag :div, class: "form-group #{'has-error' if has_errors}" do

concat form.label(field, class: 'control-label') if label

concat capture(&block)

concat errors_for(form, field)

end

end

end module LayoutHelper

def flash_messages(opts={})

@layout_flash = opts.fetch(:layout_flash) { true }



capture do

flash.each do | name, msg|

concat content_tag(:div, msg, id: "flash_#{name}")

end

end

end



def show_layout_flash?

@layout_flash.nil? ? true : @layout_flash

end

end

Step4: Now it’s time, do some validation.

account.rb model settings,

class Account < ActiveRecord::Base



RESTRICTED_SUBDOMAIN = %w(www)

belongs_to :owner, class_name: 'User'

#validates :owner, presence: true

accepts_nested_attributes_for :owner, allow_destroy: true



validates :subdomain_name, presence: true,

uniqueness: { case_sensitive: false},

format: { with: /\A[\w\-]+\Z/i, message: 'contains invalid characters' },

exclusion: {in: RESTRICTED_SUBDOMAIN, message: 'restricted'}



before_validation :downcase_account



private

def downcase_account

self.subdomain_name = subdomain_name.try(:downcase)

end

end

Step5: Do some DB work here.

below is my migration file:

def change

create_table :accounts do |t|

t.string :subdomain_name

t.integer :owner_id

t.integer :plan_id

t.string :updated_by

t.boolean :del_flag, default: false

t.timestamps null: false

end

end

Step6: Now the designing part here, it is upto you.but i did some work here. in views,

/views/accounts/new.html.erb

<% def msg(status) return "#{status}" end %>

<section id="download">

<div class="container">

<div class="row">

<div class="col-md-7 col-md-offset-3 panel panel-default">

<div class="panel-body">

<h2>Create an Acccount</h2>

<%= form_for @account do |f| %>

<%= f.fields_for :owner do |o| %>

<%= form_group_for o, :username, label: false do %>

<div class="input-group">

<span class="input-group-addon"><span class="fa fa-user fa-lg fa-fw"></span></span>

<%= o.text_field :username, class: 'form-control', placeholder: 'UserName' %>

</div>

<% end %>

<%= form_group_for o, :email, label: false do %>

<div class="input-group">

<span class="input-group-addon"><span class="fa fa-envelope fa-lg fa-fw"></span></span>

<%= o.email_field :email, class: 'form-control', placeholder: 'Email' %>

</div>

<% end %>

<%= form_group_for o, :password, label: false do %>

<div class="input-group">

<span class="input-group-addon"><span class="fa fa-key fa-lg fa-fw"></span></span>

<%= o.password_field :password, class: 'form-control', placeholder: 'Password' %>

</div>

<% end %>

<%= form_group_for o, :password_confirmation, label: false do %>

<div class="input-group">

<span class="input-group-addon"><span class="fa fa-key fa-lg fa-fw"></span></span>

<%= o.password_field :password_confirmation, class: 'form-control', placeholder: 'Confirm Password' %>

</div>

<% end %>



<%= o.hidden_field :password_updated_at, :value => Time.zone.now %>

<% end %>

<%= form_group_for f, :subdomain_name, label: false do %>

<div class="input-group">

<%= f.text_field :subdomain_name, class: 'form-control', placeholder: 'Company Name' %>

<span class="input-group-addon">

<% if Rails.env == "production" %>

.<your production name>

<% elsif Rails.env == "staging" %>

.<your staging name>

<% else %>

.demo-alokrawat050.c9users.io

<% end %>

</span>

</div>

<% end %>

<%= f.submit("Create Account", class:"btn btn-primary", data: {:confirm => msg('Do you want to create account?'), :disable_with => 'Creating'}) %>

<% end %>

</div>

</div>

</div>

</div>

</section>

and /views/welcome/index.html.erb

<div class="container">

<div class="row">

<div class="col-xs-6 col-sm-12 col-lg-6 wow fadeInUp" data-wow-delay="0.6s">

<img src="/assets/find_team_img.png" class="img-responsive" alt="feature img" width="300px" height="200px">

</div>

<div class="col-xs-12 col-sm-12 col-lg-6 wow fadeInUp templatemo-box" data-wow-delay="0.3s">

<p>If already joined with us, then please provide the your team name.</p>

<div class="form-inline">

<div class="form-group">

<%= form_tag(:action => 'find_team') do |f| %>

<%= text_field_tag 'team_name', @team_name, class: "form-control btn-lg", placeholder: "Team Name", style: "height:50px" %>

<%= button_tag(type: "submit", class: "btn btn-default") do %>

<i class="fa fa-search" aria-hidden="true"></i> Find Team

<% end %>

<%#= flash_messages layout_flash: false %>

<% end %>

</div>

</div>

</div>

</div>

</div>

Step6: Now it time to set the routes of your system.

in routes.rb file,

class SubdomainPresent

def self.matches?(request)

if Rails.env == "production"

request.subdomain.present?

elsif Rails.env == "staging"

request.subdomain.present?

else

request.subdomain.gsub!(".demo-alokrawat050","").present?

end

end

end



class SubdomainBlank

def self.matches?(request)

if Rails.env == "production"

request.subdomain.blank?

elsif Rails.env == "staging"

request.subdomain.blank?

else

request.subdomain.gsub!(".demo-alokrawat050","").blank?

end

end

end



Rails.application.routes.draw do

constraints(SubdomainPresent) do

root 'home#index', as: :subdomain_root

devise_for :users,

controllers: { invitations: 'users/invitations' }

resources :invite_users

resources :users#, only: :index

end



constraints(SubdomainBlank) do

root 'welcome#index'

resources :accounts, only: [:new, :create]

resources :welcome do

collection { post :find_team

get :find_team_index}

end

end

end

Step7: Last but not least, do some settings for apartment gem.

open /config/application.rb file,add below line.

config.middleware.use 'Apartment::Elevators::Subdomain'

and /config/initializers/apartment.rb

Apartment.configure do |config|

config.tenant_names = -> { Account.pluck(:subdomain_name) }

config.excluded_models = ['Account']

# these models will not be multi-tenanted, but remain in the global (public) namespace

Apartment::Elevators::Subdomain.excluded_subdomains = ['www', 'demo-alokrawat050', 'admin', 'public']

end

I hope this article will help you to understand, how to implement the SubDomain functionality or SAAS concept.

If you have any doubts then please share with me.

Enjoy Coding

Thanks & Best Regards,

Alok Rawat