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) — providesAshAuthentication.Phoenix.Controllerwhich 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:
- Phoenix LiveView (
phoenix_live_view) — needed for the LiveView hook (AshMultiAccount.Phoenix.LiveHook) and the account switcher component (AshMultiAccount.Phoenix.Components).
Note:
ash_authentication_phoenixalso providesAshAuthentication.Phoenix.LiveSessionwhich 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:
Using Igniter (recommended)
# 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"}
]
endManual
# 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"}
]
endRun mix deps.get to fetch the dependencies.
Using Igniter (recommended)
The Igniter installer automates steps 1–6 below:
- Adds the
AshMultiAccountextension to your User resource - Creates the LinkedAccount resource (or patches it if it exists)
- Registers LinkedAccount in your domain
- Patches your auth controller's
success/4to callput_user_id/3 - Creates a
MultiAccountController - Adds
use AshMultiAccount.Phoenix.Router, thePlug, 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
LoadMultiAccountplug (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.
endConfiguration Options
| Option | Required | Default | Description |
|---|---|---|---|
linked_account_resource | Yes | — | The LinkedAccount resource module |
active_check | No | nil | {field, value} tuple — only active users can be linked/switched to |
display_fields | No | [] | Fields loaded on users for the switcher UI |
max_linked_accounts | No | 5 | Maximum linked accounts per session |
The extension's transformer will automatically add:
- A
:linked_accountscalculation that resolves linked account records for a session - A
:get_user_with_linked_accountsread 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
endThe transformer generates the full schema automatically:
- Attributes:
session_token,status(:active/:inactive), timestamps - Relationships:
primary_userandlinked_user(bothbelongs_toyour 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
endStep 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
endNote: Replace
:YOUR_OTP_APPinclear_session/1with your OTP application name (the:appvalue in yourmix.exsproject config).
Why is
put_user_idneeded? 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/3writes 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}"
endThe controller mixin provides two actions:
link_account/2— links a newly signed-in user to an existing primary accountswitch_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
endAshMultiAccount.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
endThe 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 (nilwhen not in multi-account mode)
Tip: If your app has both LiveView and controller-rendered pages, you can add the
LoadMultiAccountplug (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
endThis 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