# Phoenix Integration
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.
```elixir
pipeline :browser do
plug :fetch_session
plug :fetch_flash
# ... other plugs ...
plug AshMultiAccount.Phoenix.Plug
end
```
The 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`.
```elixir
pipeline :browser do
plug :fetch_session
# ... other plugs ...
plug AshMultiAccount.Phoenix.Plug
plug AshMultiAccount.Phoenix.LoadMultiAccount, user_resource: MyApp.Accounts.User
end
```
### Configuration
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:
1. Loads the primary user via the `get_user_with_linked_accounts` action
2. Validates the primary user passes `active_check`
3. Resolves the current user from the session (overriding any JWT-based assign)
4. Assigns both `@current_user` and `@primary_user`
### Standard Mode
When no multi-account session exists:
1. Gets `current_user` from conn assigns or the session
2. Loads configured `display_fields`
3. Sets `@primary_user` to `nil`
### 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.Conn` pipeline)
- Use **LiveHook** for LiveView pages (`on_mount` hook)
- 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 string
- `get_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` — returns `true` if both keys are present
- `clear_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.
```elixir
use AshMultiAccount.Phoenix.Router
scope "/", MyAppWeb do
pipe_through :browser
multi_account_routes MultiAccountController, MyApp.Accounts.User
end
```
This 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:
```elixir
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.
```elixir
defmodule MyAppWeb.MultiAccountController do
use MyAppWeb, :controller
use AshMultiAccount.Phoenix.Controller,
user_resource: MyApp.Accounts.User
end
```
### Overridable 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:
1. **No authenticated user** — redirects to sign-in with a flash error
2. **Primary user matches current user** — sets up the multi-account session and redirects to sign-in (so the user can authenticate another account)
3. **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.
4. **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:
1. Renews the session ID (`configure_session(renew: true)`) for session fixation protection
2. Writes the target user's ID to the session
3. Preserves the multi-account session keys
4. 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.
```elixir
live_session :authenticated,
on_mount: [
{AshAuthentication.Phoenix.LiveSession, :load_from_session},
{AshMultiAccount.Phoenix.LiveHook, {:load_multi_account, MyApp.Accounts.User}}
] do
# ...
end
```
An optional keyword list can be passed as a third tuple element to configure the hook:
```elixir
{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):
1. Loads the primary user via the `get_user_with_linked_accounts` action
2. Validates the primary user passes `active_check`
3. Resolves the current user from the session's `"user"` key (not socket assigns, since AshAuthentication's JWT can't reflect switches)
4. Assigns both `@current_user` and `@primary_user`
### Standard Mode
When no multi-account session exists:
1. Gets `current_user` from socket assigns or the session
2. Loads configured `display_fields`
3. Sets `@primary_user` to `nil`
### 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.
```heex
<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.