2 domains, 1 app, shared MVC
How I set up our Rails app to handle multiple domains
At Pillow, we have a stack composed of a Rails backend and a React frontend.
Our application used to only serve one domain — pillowhomes.com
— until Q1 of 2017. With the launch of a brand new product, we needed to add another domain into the mix: pillo.co
.
In this post, I will explain how I set up our Rails app to handle requests from multiple domains. Along the way, I will cover two issues I ran into and how I solved them:
- Allowing cross-origin HTTP requests via cross-origin resource sharing (CORS)
- Making it work with various development environments (local, QA, staging)
We use the pillo.co
domain for a product called “Property Guide”.
(The TLDR story behind property guide is that it’s a resource for guests to use during the duration of their stay at a property connected to Pillow).
Property guides have the url pillo.co/guide/:uuid
. The uuid
is a unique, randomly generated string (using the SecureRandom ruby class) at the creation of each PropertyGuide object.
Our pillowhomes.com
domain is used for everything else.
I. Let’s start with routing setup
The property guide UI must be accessible from both domains.
The reason it needs to be viewable from the pillowhomes.com
domain is because we want to give apartment residents who will be creating a guide for their guests the ability to preview the guide before actually creating one.
This is done via inserting an iframe of pillowhomes.com/guide/preview
into the host’s dashboard. We chose to iframe the guide inside the dashboard because iframe is easy and it works extremely well with the way our system is set up.
Of course, the iframe won’t work with pillo.co/guide/:uuid
because, as mentioned, the guide hasn’t been created yet.
The paths under the guide
namespace must therefore be set up while taking into consideration the following criteria:
pillo.co
must only be available for/guide/:uuid/etc
paths. Hitting any other path within the domain will receive a 404 response from the server.pillowhomes.com
is available for/guide/preview
path only.
Shall we begin?
Setting up the routes is fairly simple and straightforward.
All you need to do is:
- Create a class to handle custom domain constraint
lib/constraints/domain_constraint.rb
class DomainConstraint def initialize(domain)
@domain = domain.split(',')
end def matches?(request)
@domain.include?(request.domain)
end
end
2. Use the DomainConstraint class to set up the routes in routes.rb
require 'constraints/domain_constraint'NameOfApp::Application.routes.draw do
constraints DomainConstraint.new(ENV['MAIN_DOMAIN']) do namespace :guide do
get 'preview'
end
end constraints DomainConstraint.new(ENV['GUIDE_DOMAIN']) do namespace :guide do
get '/:uuid', action: 'show'
get '/:uuid/*path', action: 'show'
end
end
end
MAIN_DOMAIN and GUIDE_DOMAIN variables are defined for each environment (development, QA, staging, and production) as comma separated strings.
This kind of dynamic constraint using
constraints DomainConstraint.new(domain) do
...
end
is a concept introduced in Rails 3.
Each class must have a matches?
method that returns true if the request domain is allowed to access the routes defined under the constraint.
Here is a good resource for further reading.
3. Define appropriate actions inside a GuideController
to handle the requests
class GuideController < ApplicationController def show
end def preview
render 'show'
end
end
II. Issue #1: Cross-origin HTTP request to the api
Both pillo.co
and pillowhomes.com
need to make HTTP requests (GET) to fetch backend data for specific guides.
the HTTP method and URL pairs are:
GET
'/api/property_guide/:uuid'
frompillo.co
domain
and
GET
'api/property_guide/preview
frompillowhomes.com
domain
However, because pillo.co
is a different origin than the server serving the app (http://localhost:3000
for development and https://www.pillowhomes.com
for production), you run into two problems:
- Browser refuses request due to Cross-Origin HTTP Request.
Modern browsers follow an internet security policy called “Same-origin policy.”
This means that under normal circumstances, browser restricts HTTP requests initiated from within javascript to the same origin.
Two webpages have the same origin if the protocol (http/https), port (e.g. 3000
of localhost:3000
), and host (e.g. www.pillowhomes.com
) are all the same.
Since pillo.co
is a different domain than pillowhomes.com
, when pillo.co
is making HTTP requests to api resources under pillowhomes.com
it is making a cross-origin HTTP request.
To allow cross-origin requests, you must enable cross-origin resource sharing (CORS). To do so, add the following to the api controller serving the data from the database:
class Api::PropertyGuideController < ApplicationController
after_filter :cors_set_access_control_headers
ACCEPTABLE_ORIGINS = ENV['ACCEPTABLE_REQUEST_ORIGINS'].split(',')
Before I delve into what this block of code does, I will present the second characteristic associated with cross-origin HTTP requests.
2. Browser sending an OPTIONS Request
The following logged in my terminal when pillo.co
domain tried to make requests to the localhost
domain within my dev environment:
Started OPTIONS “/api/property_guide/lalala” for 127.0.0.1ActionController::RoutingError (No route matches [OPTIONS] "/api/property_guide/lalala"):
OPTIONS method is sent as part of a “preflight request” in CORS.
A preflight request is essentially asking the server if a particular type of request is allowed before sending the actual request.
Essentially, what the code inside Api::PropertyGuideController
does is setting the response header our server makes to preflight requests.
The Access-Control-Allow-Methods
in the preflight response header specifies the methods allowed by the client to issue. In this case, only GET
request from the client is allowed.
To enable OPTIONS method, set up the routing in routes.rb
like so:
namespace :api do
namespace :property_guide do
get 'preview'
# get '/:uuid', action: 'show' no longer works
# use
match '/:uuid', action: 'show', via: [:get, :options]
end
end
III. Issue #2: Working with multiple environments
The Pillow development process involves four environments:
- Development (local)
- QA
- Staging
- Production
To get the single pillo.co
domain to work with multiple environments, we must use subdomains.
For the local environment, I have the following line in my /etc/hosts
file:
127.0.0.1 localhost local.pillo.co
This way, in my local environment, I can go to local.pillo.co:3000/guide/:uuid
to view the individual property guides.
For all other environments, we must edit the DNS settings through the DNS hosting service provider.
Pillow’s domains were purchased through Network Solutions. Through Network Solutions, we could point any subdomain to any specific DNS domain target by editing the CNAME Records.
For instance, I pointed the subdomain qa
(and thus the host qa.pillo.co
) to our Pillow QA DNS target. In the QA environment, one could view a property guide via qa.pillo.co/guide/:uuid
.
Et Voila! Everything you need to set up your Rails web application to handle multiple domains.
Pillow is hiring! Check us out: www.pillowhomes.com/about
I’m a software engineer in the city. I graduated from MIT in 2013 with a BS in biology. I started my career in tech at the start of 2015 and have been loving it since. If you like my work, follow me on Medium to receive more stories from me.