This topic explains the architecture of AshMultiAccount: the data model, session management, and the linking and switching flows.
Core Concepts
Primary User
The user who initiates account linking. When User A clicks "Add another account", User A becomes the primary user for that multi-account session. All linked accounts in the session belong to this primary user.
Linked User
A user who signs in through the linking flow and gets associated with the primary user's session. The linked user can be switched to without re-authenticating.
Session Token
A UUID generated per browser session that ties linked accounts together. It's stored in the Phoenix session and used as a filter when querying linked accounts. This means:
- Links are session-scoped — signing out and back in starts fresh
- Different browsers/devices have independent link sets
- The token is generated automatically by
AshMultiAccount.Phoenix.Plug
Data Model
AshMultiAccount uses two Spark DSL extensions that transform your resources at compile time.
User Resource (AshMultiAccount)
The extension adds to your User resource:
:linked_accountscalculation — resolves linked account records for a given session token. Implemented byAshMultiAccount.Calculations.LinkedAccountSessions.:get_user_with_linked_accountsread action — loads a user byprimary_user_idwith display fields and the linked accounts calculation. Used by the LiveView hook on every mount and by theLoadMultiAccountplug on every controller request.
LinkedAccount Resource (AshMultiAccount.LinkedAccount)
The extension generates a complete resource schema:
Attributes:
session_token(string) — the session UUID tying this link to a browser sessionstatus(atom::active/:inactive) — defaults to:activeinserted_at,updated_at(timestamps)
Relationships:
primary_user— belongs_to the User who initiated linkinglinked_user— belongs_to the User who was linked
Actions:
create_linked_account— creates a link with self-link prevention and max-account enforcementget_linked_accounts— reads links filtered by primary_user, session_token, and statusactivate/deactivate— toggle a linked account's statusread/destroy— standard CRUD
Calculations:
is_active?— boolean check on the status attribute
Identity:
- Unique on
{primary_user_id, linked_user_id, session_token}— prevents duplicate links in the same session
Linking Flow
Here's what happens when a user links a new account:
1. User A is signed in, clicks "Add another account"
↓
2. GET /link/p/:user_a_id → Controller.link_account/2
↓
3. primary_user_id matches current user → setup_multi_account_session
- Stores primary_user_id and session_token in session
- Redirects to sign-in page with return_to=/link/p/:user_a_id
↓
4. User signs in as User B → AuthController.success/4
- AshAuthentication stores User B in session
- put_user_id writes User B's ID
- Redirects to /link/p/:user_a_id (from return_to)
↓
5. GET /link/p/:user_a_id → Controller.link_account/2 (again)
↓
6. primary_user_id != current user → renders auto-submit form
- Returns minimal HTML page with a form that POSTs to the same path
- Form includes a CSRF token and auto-submits via JavaScript
- (noscript fallback: user clicks "Link Account" button)
↓
7. POST /link/p/:user_a_id → Controller.link_account/2
↓
8. Creates LinkedAccount record: primary=User A, linked=User B, session_token
- Sets primary_user_id in session
- Redirects to after_link_pathAfter linking, both users appear in the account switcher component.
Switching Flow
Here's what happens when switching to a linked account:
1. User clicks "Switch" next to User A in the switcher
↓
2. GET /link/switch_to/:user_a_id → Controller.switch_to_account/2
↓
3. Validates:
- Target user exists
- Target user passes active_check (if configured)
- Target user belongs to the current linked account group
↓
4. On success:
- Renews session ID (session fixation protection)
- Writes target user ID to session "user" key
- Preserves primary_user_id and session_token
- Redirects to after_switch_pathThe session renewal via configure_session(renew: true) is a security measure that generates a new session ID while keeping all session data intact.
Active Check
The optional active_check configuration filters out inactive users from linked account queries and prevents switching to inactive accounts.
When configured as active_check {:status, :active}:
- The
get_linked_accountspreparation adds a filter on the linked user's status field - The controller's switch action calls
validate_user_active/2before allowing a switch - The LiveView hook checks if the primary user is still active on every mount
This is useful for apps where users can be deactivated or suspended — linked accounts belonging to inactive users are automatically excluded.
Session Keys
Three session keys manage the multi-account state:
| Key | Purpose | Set By |
|---|---|---|
"user" | AshAuthentication subject string ("user?id=UUID") | Auth controller, switch action |
"primary_user_id" | UUID of the primary account | Link action |
"session_token" | UUID tying links to this session | Plug (auto-generated) |
A multi-account session is "active" when both "primary_user_id" and "session_token" are present. Both the LiveView hook (LiveHook) and the controller plug (LoadMultiAccount) check these same keys to decide whether to load linked accounts or operate in standard single-account mode. They can coexist in the same app — the hook handles LiveView pages while the plug handles controller-rendered pages, and both read from the same session state.
Compile-Time Verification
Both extensions include verifiers that run after compilation to catch configuration errors early:
- User verifier: checks that the
linked_account_resourcehas theAshMultiAccount.LinkedAccountextension, validates mutual references, and ensuresdisplay_fieldsandactive_checkfields exist on the resource - LinkedAccount verifier: checks that the
user_resourcehas theAshMultiAccountextension