2 domains, 1 app, shared MVC

How I set up our Rails app to handle multiple domains

Lily Chen
Pillow.codes
6 min readMar 3, 2017

--

Check out my mad powerpoint skills

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:

  1. Allowing cross-origin HTTP requests via cross-origin resource sharing (CORS)
  2. Making it work with various development environments (local, QA, staging)
screenshot of React-based property guide mobile view (courtesy of chrome console)

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.

screenshot of guide iframe from React-based Residential Dashboard

The paths under the guide namespace must therefore be set up while taking into consideration the following criteria:

  1. 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.
  2. 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:

  1. Create a class to handle custom domain constraint

lib/constraints/domain_constraint.rb

2. Use the DomainConstraint class to set up the routes in routes.rb

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

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

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' from pillo.co domain

and

GET 'api/property_guide/preview from pillowhomes.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:

  1. Browser refuses request due to Cross-Origin HTTP Request.
error in chrome console (local environment)

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.

Under the same-origin policy, cross-origin requests must be enabled via CORS

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:

Screenshot of source code from editor because it looks cleaner than putting it above

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:

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.

how preflight requests work

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:

III. Issue #2: Working with multiple environments

The Pillow development process involves four environments:

  1. Development (local)
  2. QA
  3. Staging
  4. 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.

If you learned something new from this article, mind clicking on the ❤ icon for me? It would mean so much!

--

--

Lily Chen
Pillow.codes

Senior software engineer at Datadog. I write about tech and life. Portfolio: https://lilychencodes.com/