This guide walks you through adding multi-account linking and switching to an existing Ash + AshAuthentication + Phoenix app. By the end, your users will be able to link multiple accounts to a single browser session and switch between them without re-authenticating.

Prerequisites

AshMultiAccount adds multi-account support on top of an existing Ash + AshAuthentication + Phoenix app. Before installing, you need:

Required

These are pulled in automatically as transitive dependencies of ash_multi_account:

  • Ash and Spark
  • AshAuthentication — you must have at least one authentication strategy configured
  • Phoenix — controllers, plugs, and router all require it

You also need:

  • A user resource — an Ash resource with AshAuthentication set up and registered in a domain (can be any module name, e.g. MyApp.Accounts.User, MyApp.Accounts.Person, etc.)
  • AshAuthentication Phoenix (ash_authentication_phoenix) — provides AshAuthentication.Phoenix.Controller which is used by the auth controller that handles sign-in callbacks. Most Phoenix + AshAuthentication apps already have this.

Don't have a user resource yet? Follow the AshAuthentication Getting Started guide to create one with an authentication strategy, then come back here.

Also needed for LiveView

If your app uses LiveView for authenticated pages, also add:

Note: ash_authentication_phoenix also provides AshAuthentication.Phoenix.LiveSession which the multi-account LiveView hook runs alongside. You likely already have it if your app uses AshAuthentication with LiveView.

The installer patches your existing resources and router; it does not create a User resource, domain, or authentication setup from scratch.

Specific version requirements are defined in the library's mix.exs — running mix deps.get will ensure compatible versions are resolved automatically.

Data Layer

AshMultiAccount is data layer agnostic. It works with any Ash data layer — AshPostgres, AshSqlite, ETS, or others. The library generates standard Ash resources with attributes, relationships, and actions that work on any data layer. Both the demo app and the library's own test suite use ETS.

Authentication Strategy

AshMultiAccount is strategy-agnostic. It works with any AshAuthentication strategy — password, OAuth2, magic links, API keys, or any combination. The library hooks into the session layer after authentication completes, so it doesn't care how the user originally signed in. The demo app uses password authentication for simplicity, but the same setup works with any strategy.

Installation

Add the dependencies to your mix.exs. What you need depends on your setup:

# mix.exs
def deps do
  [
    {:ash_multi_account, "~> 0.1.0"},
    {:igniter, "~> 0.6"},
    # If not already present:
    {:ash_authentication_phoenix, "~> 2.0"},
    # Also add if using LiveView:
    {:phoenix_live_view, "~> 1.0"}
  ]
end

Manual

# mix.exs
def deps do
  [
    {:ash_multi_account, "~> 0.1.0"},
    # If not already present:
    {:ash_authentication_phoenix, "~> 2.0"},
    # Also add if using LiveView:
    {:phoenix_live_view, "~> 1.0"}
  ]
end

Run mix deps.get to fetch the dependencies.

The Igniter installer automates steps 1–6 below:

  1. Adds the AshMultiAccount extension to your User resource
  2. Creates the LinkedAccount resource (or patches it if it exists)
  3. Registers LinkedAccount in your domain
  4. Patches your auth controller's success/4 to call put_user_id/3
  5. Creates a MultiAccountController
  6. Adds use AshMultiAccount.Phoenix.Router, the Plug, and routes to your router

Steps 7–8 (LiveView hook / controller plug and account switcher component) are app-specific — the installer prints post-install instructions for these.

mix igniter.install ash_multi_account

If ash_multi_account is a path or git dependency (not yet on Hex), run the installer directly instead:

mix ash_multi_account.install

You can also specify resource module names explicitly:

mix igniter.install ash_multi_account \
  --user MyApp.Accounts.User \
  --linked-account MyApp.Accounts.LinkedAccount

After running the installer:

  • LiveView apps: Follow the post-install instructions for the LiveView hook (Step 7) and component (Step 8)
  • Controller-only apps: Add the LoadMultiAccount plug (Step 7 alt) and component (Step 8)

If using a database-backed data layer (AshPostgres, AshSqlite), update the generated LinkedAccount resource's data layer configuration and run migrations:

mix ash.codegen create_linked_accounts
mix ash.migrate

Manual

Follow all steps below to set up AshMultiAccount manually.

Step 1: Add the Extension to Your User Resource

Add AshMultiAccount to your User resource's extensions and configure the multi_account section:

defmodule MyApp.Accounts.User do
  use Ash.Resource,
    domain: MyApp.Accounts,
    data_layer: ...,  # any Ash data layer (AshPostgres, AshSqlite, ETS, etc.)
    extensions: [AshAuthentication, AshMultiAccount]

  multi_account do
    linked_account_resource MyApp.Accounts.LinkedAccount
    active_check {:status, :active}
    display_fields [:name, :email, :avatar_url]
    max_linked_accounts 5
  end

  # ... your existing attributes, actions, etc.
end

Configuration Options

OptionRequiredDefaultDescription
linked_account_resourceYesThe LinkedAccount resource module
active_checkNonil{field, value} tuple — only active users can be linked/switched to
display_fieldsNo[]Fields loaded on users for the switcher UI
max_linked_accountsNo5Maximum linked accounts per session

The extension's transformer will automatically add:

  • A :linked_accounts calculation that resolves linked account records for a session
  • A :get_user_with_linked_accounts read action used by the LiveView hook

Step 2: Create the LinkedAccount Resource

Create a new resource with the AshMultiAccount.LinkedAccount extension:

defmodule MyApp.Accounts.LinkedAccount do
  use Ash.Resource,
    domain: MyApp.Accounts,
    data_layer: ...,  # any Ash data layer (AshPostgres, AshSqlite, ETS, etc.)
    extensions: [AshMultiAccount.LinkedAccount]

  multi_account do
    user_resource MyApp.Accounts.User
  end

  # If using a database-backed data layer, add its config here.
  # For example, with AshPostgres:
  #   postgres do
  #     table "linked_accounts"
  #     repo MyApp.Repo
  #   end
end

The transformer generates the full schema automatically:

  • Attributes: session_token, status (:active/:inactive), timestamps
  • Relationships: primary_user and linked_user (both belongs_to your User)
  • Actions: create_linked_account, get_linked_accounts, activate, deactivate, read, destroy
  • Calculations: is_active?
  • Identity: unique constraint on {primary_user_id, linked_user_id, session_token}

If using a database-backed data layer, generate and run the migration:

mix ash.codegen create_linked_accounts
mix ash.migrate

Note: In-memory data layers like ETS require no migration step.

Step 3: Register in Your Domain

Add the LinkedAccount resource to your domain:

defmodule MyApp.Accounts do
  use Ash.Domain

  resources do
    resource MyApp.Accounts.User
    resource MyApp.Accounts.LinkedAccount
  end
end

Step 4: Update the Auth Controller

Your AshAuthentication auth controller needs to write the user ID to the session in a format the multi-account hook can read. Add a call to AshMultiAccount.Phoenix.Session.put_user_id/3 in your success callback:

defmodule MyAppWeb.AuthController do
  use MyAppWeb, :controller
  use AshAuthentication.Phoenix.Controller

  def success(conn, _activity, user, _token) do
    conn
    |> store_in_session(user)
    |> AshMultiAccount.Phoenix.Session.put_user_id(user.id)
    |> assign(:current_user, user)
    |> redirect(to: ~p"/")
  end

  def failure(conn, _activity, _reason) do
    conn
    |> put_flash(:error, "Incorrect email or password")
    |> redirect(to: ~p"/sign-in")
  end

  def sign_out(conn, _params) do
    conn
    |> clear_session(:YOUR_OTP_APP)
    |> redirect(to: ~p"/sign-in")
  end
end

Note: Replace :YOUR_OTP_APP in clear_session/1 with your OTP application name (the :app value in your mix.exs project config).

Why is put_user_id needed? AshAuthentication stores a JWT subject string in the session. The multi-account hook needs a plain user ID to resolve the current user after account switches. put_user_id/3 writes the subject in a format both systems can read.

Step 5: Create the Multi-Account Controller

defmodule MyAppWeb.MultiAccountController do
  use MyAppWeb, :controller
  use AshMultiAccount.Phoenix.Controller,
    user_resource: MyApp.Accounts.User

  # Optionally override redirect paths:
  # def after_link_path(_conn), do: ~p"/"
  # def after_switch_path(_conn), do: ~p"/"
  # def sign_in_path(_conn, primary_user_id), do: ~p"/sign-in?return_to=/link/p/#{primary_user_id}"
end

The controller mixin provides two actions:

  • link_account/2 — links a newly signed-in user to an existing primary account
  • switch_to_account/2 — switches the session to a different linked user

Step 6: Add Routes

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

  pipeline :browser do
    # ... existing plugs ...
    plug AshMultiAccount.Phoenix.Plug
  end

  scope "/", MyAppWeb do
    pipe_through :browser

    # Generates:
    #   GET  /link/p/:primary_user_id  -> MultiAccountController.link_account
    #   POST /link/p/:primary_user_id  -> MultiAccountController.link_account
    #   GET  /link/switch_to/:user_id  -> MultiAccountController.switch_to_account
    multi_account_routes MultiAccountController, MyApp.Accounts.User
  end
end

AshMultiAccount.Phoenix.Plug ensures a session token UUID exists before any multi-account routes are hit.

Step 7: LiveView Setup

Skip this step if your app doesn't use LiveView. See Step 7 alt instead.

Add the multi-account hook to your authenticated live sessions. It should run after AshAuthentication's hook:

live_session :authenticated,
  on_mount: [
    {AshAuthentication.Phoenix.LiveSession, :load_from_session},
    {AshMultiAccount.Phoenix.LiveHook, {:load_multi_account, MyApp.Accounts.User}}
  ] do
  live "/", HomeLive
  # ... more live routes
end

The hook sets two assigns on every mount:

  • @current_user — the user currently acting (may differ from primary after a switch)
  • @primary_user — the primary account owner (nil when not in multi-account mode)

Tip: If your app has both LiveView and controller-rendered pages, you can add the LoadMultiAccount plug (Step 7 alt) alongside the LiveView hook. They read the same session keys and coexist without conflict.

Step 7 alt: Controller-Only Setup

Skip this step if you already added the LiveView hook above and don't have controller-rendered pages that need multi-account assigns.

Add the LoadMultiAccount plug to your browser pipeline:

pipeline :browser do
  # ... existing plugs ...
  plug AshMultiAccount.Phoenix.Plug
  plug AshMultiAccount.Phoenix.LoadMultiAccount, user_resource: MyApp.Accounts.User
end

This plug sets the same @current_user and @primary_user assigns on conn that the LiveView hook sets on the socket. It must run after :fetch_session and AshMultiAccount.Phoenix.Plug.

For controller-only apps, this is the only integration step needed — the plug handles user resolution and multi-account switching for all controller-rendered pages.

Step 8: Add the Account Switcher Component

Use the slot-based component in your layout or navigation:

<AshMultiAccount.Phoenix.Components.account_switcher
  current_user={@current_user}
  primary_user={@primary_user}
>
  <:account :let={account}>
    <.link :if={!account.current?} href={account.switch_url}>
      {account.user.name}
    </.link>
    <span :if={account.current?}>{account.user.name} (current)</span>
  </:account>
  <:add_account :let={url}>
    <.link href={url}>Add another account</.link>
  </:add_account>
</AshMultiAccount.Phoenix.Components.account_switcher>

The component imposes no styling — you control all HTML and CSS through slots. It works identically in both LiveView templates and controller-rendered templates — it's a standard Phoenix.Component that only needs @current_user and @primary_user assigns.

What's Next?

  • How It Works — understand the data model, session tokens, and linking/switching flows
  • Phoenix Integration — deep dive into each Phoenix module
  • Testing — set up test support and write tests for multi-account flows