AshMultiAccount provides seven Phoenix modules that handle session management, routing, controller actions, LiveView state, controller state, and UI components. This topic covers each module in detail.
Plug
AshMultiAccount.Phoenix.Plug ensures a session token UUID exists before any multi-account routes are hit.
pipeline :browser do
plug :fetch_session
plug :fetch_flash
# ... other plugs ...
plug AshMultiAccount.Phoenix.Plug
endThe plug generates a UUID via Ash.UUID.generate() and stores it in the "session_token" session key. If a token already exists, the plug is a no-op. This runs on every request in the pipeline, so the token is always available when the controller needs it.
LoadMultiAccount Plug
AshMultiAccount.Phoenix.LoadMultiAccount resolves @current_user and @primary_user assigns for controller-rendered pages. It mirrors the logic of the LiveView hook but operates on Plug.Conn.
pipeline :browser do
plug :fetch_session
# ... other plugs ...
plug AshMultiAccount.Phoenix.Plug
plug AshMultiAccount.Phoenix.LoadMultiAccount, user_resource: MyApp.Accounts.User
endConfiguration
The plug requires a :user_resource option — it raises ArgumentError at compile time if omitted. It must run after :fetch_session and AshMultiAccount.Phoenix.Plug in the pipeline.
Multi-Account Mode
When multi_account_session? is true:
- Loads the primary user via the
get_user_with_linked_accountsaction - Validates the primary user passes
active_check - Resolves the current user from the session (overriding any JWT-based assign)
- Assigns both
@current_userand@primary_user
Standard Mode
When no multi-account session exists:
- Gets
current_userfrom conn assigns or the session - Loads configured
display_fields - Sets
@primary_usertonil
Error Handling
When the primary user is not found or not active (e.g., stale session after data reset), the plug clears the multi-account session keys and falls back to standard mode — resolving the current user from conn.assigns or the session. For unexpected errors (e.g., database failures), the plug assigns nil gracefully and lets the controller decide how to respond.
When to Use LoadMultiAccount vs LiveHook
- Use LoadMultiAccount for controller-rendered pages (
Plug.Connpipeline) - Use LiveHook for LiveView pages (
on_mounthook) - Both can coexist in the same app — they read the same session keys
Session Helpers
AshMultiAccount.Phoenix.Session provides read/write functions for the three multi-account session keys. All read functions accept both Plug.Conn (controllers) and raw session maps (LiveView hooks). Write functions require Plug.Conn.
Key functions:
get_user_id/1/put_user_id/3— reads/writes the"user"subject stringget_primary_user_id/1/put_primary_user_id/2— reads/writes"primary_user_id"get_session_token/1/put_session_token/2— reads/writes"session_token"put_multi_account_session/3— atomically sets both"primary_user_id"and"session_token"multi_account_session?/1— returnstrueif both keys are presentclear_multi_account_session/1— removes multi-account keys (keeps"user")
The "user" key uses AshAuthentication's subject format: "<short_name>?id=<UUID>". The put_user_id/3 function constructs this format; get_user_id/1 parses it back to a plain UUID. The default short name is "user" — pass a third argument if your resource uses a different one.
Router
AshMultiAccount.Phoenix.Router provides the multi_account_routes/3 macro.
use AshMultiAccount.Phoenix.Router
scope "/", MyAppWeb do
pipe_through :browser
multi_account_routes MultiAccountController, MyApp.Accounts.User
endThis generates three routes:
| Method | Route | Action | Purpose |
|---|---|---|---|
| GET | /link/p/:primary_user_id | :link_account | Initiate linking or render auto-submit form |
| POST | /link/p/:primary_user_id | :link_account | Complete account linking (creates record) |
| GET | /link/switch_to/:user_id | :switch_to_account | Switch to a linked account |
Custom paths are supported:
multi_account_routes MultiAccountController, MyApp.Accounts.User,
link_path: "/accounts/link/:primary_user_id",
switch_path: "/accounts/switch/:user_id"Controller
AshMultiAccount.Phoenix.Controller is a macro that injects link_account/2 and switch_to_account/2 into your controller.
defmodule MyAppWeb.MultiAccountController do
use MyAppWeb, :controller
use AshMultiAccount.Phoenix.Controller,
user_resource: MyApp.Accounts.User
endOverridable Functions
| Function | Default | Purpose |
|---|---|---|
after_link_path/1 | Origin page (from session) or "/" | Redirect after successful link |
after_switch_path/1 | Origin page (from Referer) or "/" | Redirect after successful switch |
sign_in_path/2 | "/sign-in?return_to=/link/p/:id" | Where to send unauthenticated users |
sign_out_path/1 | "/sign-out" | Where to send users on fatal errors |
By default, both after_link_path/1 and after_switch_path/1 return the user to the page they were on when they started the action. For linking, the origin page is saved in the session at the start of the multi-step flow. For switching, the origin is read from the HTTP Referer header. Both fall back to "/" if the origin cannot be determined.
link_account/2
Handles three cases:
- No authenticated user — redirects to sign-in with a flash error
- Primary user matches current user — sets up the multi-account session and redirects to sign-in (so the user can authenticate another account)
- Different user (GET) — renders a minimal auto-submitting HTML form that immediately POSTs with a CSRF token. This preserves REST semantics (no record creation on GET) while working with the 302 redirect from the auth callback.
- Different user (POST) — creates a LinkedAccount record tying the current user to the primary, then redirects to
after_link_path
The GET→auto-submit→POST pattern is the same approach used by OAuth, SAML, and payment gateway flows for state-changing operations after redirects.
switch_to_account/2
Validates the switch is authorized, then:
- Renews the session ID (
configure_session(renew: true)) for session fixation protection - Writes the target user's ID to the session
- Preserves the multi-account session keys
- Redirects to
after_switch_path
Validation checks:
- Target user exists and passes
active_check - Both the current user and target user belong to the same linked account group
LiveView Hook
AshMultiAccount.Phoenix.LiveHook resolves @current_user and @primary_user on every LiveView mount.
live_session :authenticated,
on_mount: [
{AshAuthentication.Phoenix.LiveSession, :load_from_session},
{AshMultiAccount.Phoenix.LiveHook, {:load_multi_account, MyApp.Accounts.User}}
] do
# ...
endAn optional keyword list can be passed as a third tuple element to configure the hook:
{AshMultiAccount.Phoenix.LiveHook, {:load_multi_account, MyApp.Accounts.User, sign_out_path: "/logout"}}| Option | Default | Purpose |
|---|---|---|
:sign_out_path | "/sign-out" | Where to redirect on fatal errors (inactive user, load failure) |
Multi-Account Mode
When multi_account_session? is true (both "primary_user_id" and "session_token" are in the session):
- Loads the primary user via the
get_user_with_linked_accountsaction - Validates the primary user passes
active_check - Resolves the current user from the session's
"user"key (not socket assigns, since AshAuthentication's JWT can't reflect switches) - Assigns both
@current_userand@primary_user
Standard Mode
When no multi-account session exists:
- Gets
current_userfrom socket assigns or the session - Loads configured
display_fields - Sets
@primary_usertonil
Error Handling
- If the primary user is not found (e.g., stale session after data reset), the hook falls back to standard mode — resolving the current user from the session or socket assigns without multi-account context
- If the primary user is not active, the hook halts with a flash error and redirects to the configured
sign_out_path(default:/sign-out) - If loading fails with an unexpected error, the hook halts with a generic error message
Components
AshMultiAccount.Phoenix.Components provides a slot-based account switcher that imposes no styling.
<AshMultiAccount.Phoenix.Components.account_switcher
current_user={@current_user}
primary_user={@primary_user}
>
<:account :let={account}>
<div class={if account.current?, do: "font-bold", else: ""}>
<.link :if={!account.current?} href={account.switch_url}>
{account.user.name}
</.link>
<span :if={account.current?}>{account.user.name}</span>
<span :if={account.primary?}>(primary)</span>
</div>
</:account>
<:add_account :let={url}>
<.link href={url}>+ Add another account</.link>
</:add_account>
</AshMultiAccount.Phoenix.Components.account_switcher>Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
current_user | map | required | Currently active user struct |
primary_user | map | nil | Primary account owner (nil = standard mode) |
switch_path | string | "/link/switch_to" | Base path for switch URLs |
sign_in_path | string | "/sign-in" | Sign-in path for add-account URL |
link_path | string | "/link/p" | Link path for add-account URL |
Slot Data
Each :account slot receives a map with:
| Key | Type | Description |
|---|---|---|
user | struct | The user struct with display fields loaded |
current? | boolean | Whether this is the currently active user |
primary? | boolean | Whether this is the primary account |
switch_url | string | URL to switch to this account |
The :add_account slot receives the URL string for initiating a new link (includes return_to parameter).
Single-Account Mode
When primary_user is nil, the component shows just the current user and the "add account" link. The first link click establishes the multi-account session.