Dealing with multiple user schemas

by Dan Schultzer on 12 September 2019

Pow can handle multiple user schemas out-of-the-box in umbrella projects, or can be configured to handle it in the same Phoenix endpoint.

In this guide we’ll go through the following three options:

  1. Use roles to diffentiate the users
  2. Use umbrella project to keep user structs in different endpoint
  3. User structs in same endpoint

It’s important that you evaulate your project requirements. Each option gets progressively more complex.

In the following examples we’ll imagine that we work with two types of users: User and admin.

1. User roles

This is the simplest solution as you won’t have to deal with multiple Pow contexts and namespacing. You should head over to the user roles guide in the Pow docs if a roles setup fit your project requirements.

2. Umbrella project

The umbrella project is an easy solution if you have different user structs. Usually the admin and user dashboard are separated, so it’s a natural step to also have them set up as individual Phoenix apps. It may even make development and maintainance easier.

Pow will namespace cookies and sessions with the :otp_app name, so all you have to do is to set both Phoenix apps up with Pow.

No additional configuration is required:

# apps/my_app_web/config/config.exs
config :my_app_web, :pow,
  repo: MyApp.Repo,
  user: MyApp.Users.User

# apps/my_app_web/lib/my_app_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app_web

  # ...

  plug Pow.Plug.Session, otp_app: :my_app_web

  plug MyAppWeb.Router
end
# apps/my_app_admin/config/config.exs
config :my_app_admin, :pow,
  repo: MyApp.Repo,
  user: MyApp.Users.Admin

# apps/my_app_admin/lib/my_app_admin/endpoint.ex
defmodule MyAppAdmin.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app_admin

  # ...

  plug Pow.Plug.Session, otp_app: :my_app_admin

  plug MyAppAdmin.Router
end

3. Same endpoint

We’ll have to ensure that everything is namespaced right when we deal with multiple Pow configurations in the same endpoint.

It’s assumed that you already have set up your Phoenix app with one Pow configuration, and we’ll add the second user type (the admin in this example).

First configure the endpoint so we ensure that the admin user can be loaded:

defmodule MyAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app

  # ...

  plug Pow.Plug.Session,
    repo: MyApp.Repo,
    user: MyApp.Users.Admin,
    current_user_assigns_key: :current_admin,
    session_key: "admin_auth"
  
  plug Pow.Plug.Session, otp_app: :my_app

  plug MyAppWeb.Router
end

Now we’ll add Pow routes for the admin user:

defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  use Pow.Phoenix.Router

  # ... pipelines

  pipeline :pow_admin do
    plug :set_pow_config,
      repo: MyApp.Repo,
      user: MyApp.Users.Admin,
      current_user_assigns_key: :current_admin,
      session_key: "admin_auth",
      routes_backend: MyAppWeb.Pow.AdminRoutes,
      plug: Pow.Plug.Session
  end

  defp set_pow_config(conn, config), do: Pow.Plug.put_config(conn, config)

  scope "/" do
    pipe_through :browser

    pow_routes()
  end

  scope "/admin", as: :admin do
    pipe_through [:browser, :pow_admin]
    pow_routes()
  end

  # ... routes
end

We’ll have to ensure all paths generated within that pipeline are admin paths with our custom :routes_backend module. This module will prepend the controller module name with Admin so that admin_pow_* route helpers are called instead of the pow_* routes:

defmodule MyAppWeb.Pow.AdminRoutes do
  use Pow.Phoenix.Routes

  def path_for(conn, verb, vars \\ [], query_params \\ []) do
    plug = Module.concat(["AdminPow", verb])
    Pow.Phoenix.Routes.path_for(conn, plug, vars, query_params)
  end

  def url_for(conn, verb, vars \\ [], query_params \\ []) do
    plug = Module.concat(["AdminPow", verb])
    Pow.Phoenix.Routes.url_for(conn, plug, vars, query_params)
  end
end

An umbrella project is still preferred since it’s much easier when dealing with multiple Pow contexts.