# How It Works 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: | 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_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