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:

  1. :linked_accounts calculation — resolves linked account records for a given session token. Implemented by AshMultiAccount.Calculations.LinkedAccountSessions.

  2. :get_user_with_linked_accounts read action — loads a user by primary_user_id with display fields and the linked accounts calculation. Used by the LiveView hook on every mount and by the LoadMultiAccount plug 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 session
  • status (atom: :active / :inactive) — defaults to :active
  • inserted_at, updated_at (timestamps)

Relationships:

  • primary_user — belongs_to the User who initiated linking
  • linked_user — belongs_to the User who was linked

Actions:

  • create_linked_account — creates a link with self-link prevention and max-account enforcement
  • get_linked_accounts — reads links filtered by primary_user, session_token, and status
  • activate / deactivate — toggle a linked account's status
  • read / 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_path

After 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_path

The 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_accounts preparation adds a filter on the linked user's status field
  • The controller's switch action calls validate_user_active/2 before 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:

KeyPurposeSet By
"user"AshAuthentication subject string ("user?id=UUID")Auth controller, switch action
"primary_user_id"UUID of the primary accountLink action
"session_token"UUID tying links to this sessionPlug (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_resource has the AshMultiAccount.LinkedAccount extension, validates mutual references, and ensures display_fields and active_check fields exist on the resource
  • LinkedAccount verifier: checks that the user_resource has the AshMultiAccount extension