A common requirement for applications is to have a subdomain per customer that users belonging to that customer can visit. Example of this include: Slack (https://elixir.slack.com, https://phoenix.slack.com, etc.) and ReadMe.
This blog post will go through how to set up your Phoenix application so that it can be used in the same way.
The source code for this repository is available at https://github.com/Gazler/phoenix-subdomain-demo - there is a commit to represent each step in this post.
Getting Started
The first thing we will need is a new Phoenix application. Since it is focused on subdomains, I am going to call it subdomainer:
mix phoenix.new subdomainer
Once the app has been created and all the dependencies have been installed, start the app and visit it at http://localhost:4000.
mix phoenix.server
Next we need to ensure that it is accessible via a separate domain and subomains, so the following needs to be added to your /etc/hosts
file:
127.0.0.1 subdomainer.dev foo.subdomainer.dev bar.subdomainer.dev
With these additions, you should also be able to access the application via: http://subdomainer.dev:4000, http://foo.subdomainer.dev:4000 and http://bar.subdomainer.dev:4000
Currently these all point to the same page, but we are going to modify it so that it displays information about the particular app that we are trying to visit.
Determining If A Subdomain Has Been Set
With the app as it stands, if a user visits http://subdomainer.dev:4000 then we want to display the default Phoenix page that was generated with the application. However if they visit http://foo.subdomainer.dev:4000 or any subdomain then we want to show a different page. For the moment, we won’t worry too much about which subdomain is being viewed, only that there is a subdomain present.
We need to configure the application so that it knows which domain is the root domain. This is because you cannot make any guarantees about the number of subdomains. In this instance we know that subdomainer.dev
is the root and foo.subdomainer.dev
is a subdomain - however our root could be app.subdomainer.dev
and the subdomain could be foo.app.subdomainer.dev
Replace the following in config/config.exs
under the config :subdomainer, Subdomainer.Endpoint
block that should be at the top of your file:
1
|
|
With:
1
|
|
We now need to update our endpoint so it knows if a subdomain has been provided in the URL. Create a file lib/subdomainer/plugs/subdomain.ex
file with the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
This code here implements the call/2
function expected by plug. This second argument we expect is the module that will be used if a subdomain is found. This also needs to be added to lib/subdomainer/endpoint.ex
so that we can ensure our plug is called before the router. Add the following line before the plug :router, Subdomainer.Router
line:
1
|
|
You will need to restart your server and start it again (with mix phoenix.server
) every time you make a change to a file in the lib
directory as only changes in the web
directory do not require a reload.
You can validate that this is working by visiting http://subdomainer.dev:4000 which will still work as before, however if you visit http://foo.subdomainer.dev:4000 then you will see an error:
undefined function: Subdomainer.SubdomainRouter.init/1 (module Subdomainer.SubdomainRouter is not available)
This is because this router does not exist yet.
Adding The Subdomain Router
To fix the error we just need to create the SubdomainRouter at web/subdomain_router.ex
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
This fixes the error and now both http://subdomainer.dev:4000 and http://foo.subdomainer.dev:4000 work as before.
We want the subdomain pages to route somewhere else though - we can simply modify the scope
block in the new router to point to a different controller by changing it to:
1 2 3 4 5 |
|
This will error until you create the required controller, so create web/controllers/subdomain/page_controller.ex
:
1 2 3 4 5 6 7 8 |
|
You will note that PageController
has been used as the name both times. This name is not important, it just needs to match the path from the scope
block in the router.
This will work, but you will probably see the following error in your terminal:
(exit) an exception was raised: ** (Plug.Conn.AlreadySentError) the response was already sent
This is simple to fix - we just need to prevent additional plugs from running if a subdomain is found modify lib/subdomainer/plugs/subdomain.ex
to include a call to Plug.Conn.halt/1:
1 2 3 4 5 6 7 8 9 |
|
Customize response based on subdomain
The last thing to do for this is to customize the response based on the which subdomain has been visited. To do this, we will add it to the private
storage that exists in a Plug.Conn
which you can read about in the Plug docs
We will do this in the Subdomainer.Router
module where we did the initial check to see if a subdomain exists by modifying the call
function:
1 2 3 4 5 6 7 8 9 10 |
|
We can then retrieve this value in the index action of our Subdomainer.Subdomain.PageController
like so:
1 2 3 |
|
And that’s it! All of the following pages should work and show the correct page http://subdomainer.dev:4000, http://foo.subdomainer.dev:4000 and http://bar.subdomainer.dev:4000
From here you can extend this to the needs your app. One common task for an application with this feature is to lookup an application from your database based on the subdomain provided and show a 404 if one is not found. Since you know by the time the Subdomainer.SubdomainRouter
is reached in the request, the subdomain will be set, you could add a plug
to the pipeline
to perform this check before the controller action is reached.