# 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
