Create a multi-tenant, whitelabel application in Elixir & Phoenix part I: Working with subdomains
In this series of posts of I’ll explain what whitelabel, multi-tenant applications are and how a basic example of that could look like in Elixir and Phoenix.
The posts in these series are:
- Part I: Working with subdomains
- Part II: Dynamic subdomain routing
- Part III: Adding accounts (tenants)
What is a multi-tenant application?
A multi-tenant application is an application that uses the same application and database to serve multiple tenants whilst keeping each tenant’s data separated. You can think of a tenant as an account that represents a business and/or a group of people. A known example of such an app is Slack.
What is a whitelabel application?
A whitelabel application is a software application that uses the above multi-tenant approach but takes it a step further by allowing each customer (tenant) to add their own unique customisation and branding. In this setup each tenant has their own domain through which they access the application. Some examples are Shopify and Squarespace.
Architecture of a whitelabel application
There are many ways one can structure an application and this is true for whitelabel applications too. Below is an example architecture diagram for what we will be building. A whitelabel application to represent software that can be used by different farmer’s markets. The app will be accessible via the root domain (
fresco.com) and by two subdomains representing two different accounts. Requests coming from the root domain will be handled by the existing router whereas requests from the subdomains will be handled by subdomain router we will create. The subdomains will also be served different pages than then root domain:
Time to code
The requirements for this tutorial are the following:
- Elixir 1.14
- Phoenix 1.6
- Postgres 13
For more detailed instructions on how to install the above, see the official Phoenix installation instructions.
The first thing we need to do is create a new Phoenix application:
mix phx.new fresco
Once our application is created, run the relevant commands to set up the database and start the server in an interactive Elixir shell:
iex -S mix phx.server
Visit http://localhost:4000 and you should see the following welcome page:
Now that we have our server up and running, let’s move towards making it accessible via different subdomains. For a real production environment we would need to purchase a custom domain, an SSL certificate and set up the relevant DNS records. For this tutorial we will simply modify our local network settings to simulate the effect of having a custom domain. To do so, we need to modify the
Note: you will likely need to use admin permissions to edit this file (
sudo vim /etc/hosts)
Note: for Windows users you may need to edit a different file according to your system. An example is
Let’s edit the
/etc/hosts file and add 3 domains:
127.0.0.1 fresco.com green.fresco.com farm.fresco.com
What we are doing here is pointing 3 domains (
fresco.com green.fresco.com farm.fresco.com) to our local server (
127.0.0.1) for DNS resolution. This file essentially takes the 3 domains and translates them to an IP address, in this case our localhost.
Head back to the browser and type
http://fresco.com:4000/ in the existing tab. Open another two browser tabs and type
http://farm.fresco.com:4000. You should see the same page in all tabs but now notice that you are using the custom domains to access the Phoenix app:
Excellent! Now our app is accessible through different domains. Of course we still have an issue which is that we’re seeing the same content and that’s not our goal. Our goal is to see different content based on the given domain/subdomain.
To achieve this we need to establish which domain will be our root domain and also add a mechanism that captures a given subdomain.
To set our root domain, we need to make a change in our
config/config.exs file and change the host our
FrescoWeb.Endpoint is using to point to
Now let’s create a new directory called
plugs under the existing
Inside this new directory we’ll create a new file called
Inside this file we’ll add the following code:
Here we are defining a custom
Plug (what is a Plug?) that is responsible for extracting the subdomain from the incoming request and storing it inside the
connstruct which is used to store details related to the request/response lifecycle. The
init function is used to initialise the plug with different configuration options which are then passed as the second argument to the
call function. We utilise this mechanism by defining a map containing the root host we defined earlier. Inside the
callfunction we check if there is a subdomain and if so we store it inside the
conn struct, otherwise we return the
conn struct as is.
Now that we have the above plug, we need to *plug it* into the existing Phoenix pipeline which is comprised of other plugs applying various transformations. To do so, we need to edit our
Endpoint (what is an Endpoint?) located in the
lib/fresco_web/endpoint.ex file. We’ll add our
Subdomain plug just above the
FrescoWeb.Router plug like so:
To have visual confirmation that the above changes are working, we will edit the rendered html found in
lib/fresco_web/templates/page/index.html.heex to display a different greeting based on the subdomain used:
In the above snippet, we access the
@conn variable which is available to our controllers and templates as part of Phoenix and from it we extract the value of the
:subdomain key which is stored under the
privatemap. If no value is found, we return
"fresco" to indicate we’re accessing our root domain.
Restart the server, open three new tabs and access the domains we listed above http://fresco.com:4000/, http://green.fresco.com:4000/, http://farm.fresco.com:4000/. You should see the following pages:
Awesome! Our app can now be accessed through different subdomains and displays content that’s relevant to each subdomain . Of course we have more to do here to make this more dynamic but this is a great step that lays the foundations for our next article in the series. See you soon!