Guidance on how to build a Phoenix app with user pages on custom subdomains

Hi,

I’m working on a Phoenix app and I would like to implement the feature that we see on many websites where each user have its own page on a custom subdomain.

user1.example.com
user2.example.com
user3.example.com

As I’ve never built something like that, could someone please give me some guidance on how to do it?

1 Like

Do you need to serve on example.com as well as subdomains? If not, then you can add a plug before the router in endpoint.ex that extracts the subdomain from conn.host and assigns some necessary info to distinguish between users into conn.assigns.

lib/app_web/endpoint.ex

# ...
plug AppWeb.Plugs.UserFromHost
plug AppWeb.Router
# ...

lib/app_web/plugs/user_from_host.ex

defmodule AppWeb.Plugs.UserFromHost do
  @behaviour Plug
  import Plug.Conn
  
  @impl true
  def init(opts), do: opts

  @impl true
  def call(conn, _opts) do
    case String.split(conn.host, ".") do
      [sub, "example", "com"] -> assign_user(conn, sub)
      _other -> conn
    end
  end

  defp assign_user(conn, sub) do
    assign(conn, :user, ...)
  end
end
1 Like

Yes,

www.example.com

is the main website and then every user should have their page at

username.example.com

.

Then you can replace the plug Router in endpoint with a plug that calls either the main website router or a user router based on conn.host.

lib/app_web/endpoint.ex

# ...
plug AppWeb.Plugs.RouterForwarder
# plug AppWeb.Router <- remove this line
# ...

lib/app_web/plugs/router_forwarder.ex

defmodule AppWeb.Plugs.RouterForwarder do
  @behaviour Plug
  import Plug.Conn
  
  @impl true
  def init(opts), do: opts

  @impl true
  def call(conn, opts) do
    case String.split(conn.host, ".") do
      ["www", "example", "com"] -> AppWeb.Router.call(conn, opts) # the router from `endpoint.ex` moved here
      [sub, "example", "com"] -> conn |> assign_user(sub) |> AppWeb.UserRouter.call(opts)
      ["example", "com"] -> AppWeb.Router.call(conn, opts) # and here
    end
  end

  defp assign_user(conn, sub) do
    assign(conn, :user, ...)
  end
end
6 Likes

An even simpler solution is to use a reverse proxy to set the subdomain as a path param. This would require no code change.

2 Likes

In my experience this does “lose” the subdomain on navigation, and requires lots of repetition threading the path param into every link helper. May be ways to reconcile that, tho

We let the AWS load balancer handle this. Very easy to set up.

https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-update-rules.html

Hi, it is possible to use the phoenix router scope, to dispatch by request host. I haven’t used it yet and don’t know, if the routes.Helper supports this, but see:
https://hexdocs.pm/phoenix/Phoenix.Router.html#scope/2