
# Post-Login Landing

After sign-in, different users should land on different pages — an admin on the admin home, a customer on their record (or a picker when they have several). Sovrium expresses this as **auth configuration**, not per-page redirect logic: each role declares a `defaultLanding`, the `auth` block declares a `landingPath` mount point, and `noAccessPath` handles the unmatched case.

```yaml
auth:
  strategies:
    - type: emailAndPassword
  scopeTables: [clients]
  defaultRole: customer-admin
  landingPath: /portal
  noAccessPath: /403
  roles:
    - name: engineer
      defaultLanding: /admin
    - name: customer-admin
      defaultLanding: /portal/clients/$currentUser.assignments.clients[0]
      pickerLanding: /portal/select/clients
```

## Configuration Fields

Two fields on `auth`, two on each role:

| Field            | Level     | Description                                                                                                                                                                                            |
| ---------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `landingPath`    | `auth`    | Engine-resolver mount path. Sessions navigating here are redirected to the matching role's `defaultLanding`. Must start with `/`. Required whenever any role sets `defaultLanding` or `pickerLanding`. |
| `noAccessPath`   | `auth`    | Fallback path when no role's `defaultLanding` matches the session. Must start with `/`. Defaults to `/403`.                                                                                            |
| `defaultLanding` | `roles[]` | Per-role landing URL. Must start with `/`. May contain at most one `$currentUser.assignments.<table>[0]` token.                                                                                        |
| `pickerLanding`  | `roles[]` | Multi-record fallback for a _templated_ `defaultLanding`. Must start with `/`. Must **not** contain any assignment token.                                                                              |

:::callout
`landingPath` is **user-controlled** — pick `/portal`, `/dashboard`, `/home`, or anything else. There is no engine convention. The same is true of `pickerLanding`: `/portal/select/clients`, `/companies-picker`, whatever you like.
:::

## Resolution Logic

When an authenticated session navigates to `auth.landingPath`, the engine walks `auth.roles[]` in **declaration order** and applies the first role the user holds. For that role's `defaultLanding`:

| `defaultLanding` shape           | Assignment count for the templated `<table>` | Engine action                                    |
| -------------------------------- | -------------------------------------------- | ------------------------------------------------ |
| Bare URL (no token)              | n/a                                          | Redirect to `defaultLanding` unconditionally.    |
| Templated (one token)            | Exactly one assignment                       | Substitute the assignment ID, redirect there.    |
| Templated (one token)            | More than one assignment                     | Redirect to the role's `pickerLanding`.          |
| No role matched / no assignments | n/a                                          | Redirect to `noAccessPath` (defaults to `/403`). |

## `$currentUser.assignments` Interpolation

The `$currentUser.assignments.<table>` token resolves to the set of record IDs from the user's `user_access` rows for `<table>` (a [scope table](/en/docs/auth-sessions)). It appears in two distinct ways:

**In a `defaultLanding` URL** — the single-record form `$currentUser.assignments.<table>[0]` substitutes the _first_ assignment ID, producing a deep link straight to that record:

```yaml
roles:
  - name: customer-admin
    defaultLanding: /portal/clients/$currentUser.assignments.clients[0]
    pickerLanding: /portal/select/clients
```

A role may contain **at most one** such token (the resolver cannot infer which scope to count with two). A `pickerLanding` must contain **none** — by definition the multi-record case has no single ID to substitute.

**In a `dataSource.filter`** — the un-indexed form `$currentUser.assignments.<table>` resolves to the full ID list, so the picker page lists exactly the records the user can reach:

```yaml
pages:
  - name: client-picker
    path: /portal/select/clients
    access: authenticated
    components:
      - type: list
        props: { id: clients }
        dataSource:
          table: clients
          filter:
            - field: id
              operator: in
              value: $currentUser.assignments.clients
        children:
          - type: text
            content: $record.name
```

## The `landingPath` Page Is the Access Guard

`landingPath` must be backed by a **co-located page** declared at the same `path`. That page is the unauthenticated-access guard: its `access` block bounces anonymous visitors to `/login` before the landing resolver runs. For authenticated visitors the resolver redirects away before this (typically empty) page renders.

```yaml
auth:
  landingPath: /portal
  # ...

pages:
  - name: portal-landing
    path: /portal
    access:
      require: authenticated
      redirectTo: /login
    components: [] # never rendered for authed users — resolver redirects first
```

:::callout
Forgetting the co-located guard page is the most common mistake: `landingPath: /portal` with no `/portal` page leaves anonymous visitors unguarded. Always pair the mount path with a page that requires authentication.
:::

## Full Example

```yaml
auth:
  strategies:
    - type: emailAndPassword
  scopeTables: [clients]
  defaultRole: customer-admin
  landingPath: /portal
  noAccessPath: /403
  roles:
    - name: engineer
      defaultLanding: /admin
    - name: customer-admin
      defaultLanding: /portal/clients/$currentUser.assignments.clients[0]
      pickerLanding: /portal/select/clients

pages:
  - name: portal-landing
    path: /portal
    access: { require: authenticated, redirectTo: /login }
    components: []
  - name: client-detail
    path: /portal/clients/:id
    access: authenticated
    components: []
  - name: client-picker
    path: /portal/select/clients
    access: authenticated
    components: []
  - name: admin-home
    path: /admin
    access: [engineer]
    components: []
  - name: forbidden
    path: /403
    access: authenticated
    components: []
```

In this app: an `engineer` lands on `/admin`; a `customer-admin` with one client lands on that client's detail page; with several, on the picker; and anyone the resolver cannot place hits `/403`.

## Related Pages

- [Roles & RBAC](/en/docs/auth-roles-rbac) — where `defaultLanding` / `pickerLanding` are defined on roles.
- [Sessions](/en/docs/auth-sessions) — `scopeTables`, `user_access`, and `$currentUser.activeAssignment`.
- [Pages](/en/docs/pages-overview) — page `path`, `access`, and `dataSource.filter`.
