
# Assignments & Landing Guards

[Post-login landing](/en/docs/auth-post-login) routes a session to the right page. Two mechanisms make that routing _correct_: the token that resolves which records a user may reach, and the page that stops an anonymous visitor before the resolver ever runs.

## The Assignment Token

`$currentUser.assignments.<table>` resolves to the set of record IDs from the user's `user_access` rows for `<table>` — a [scope table](/en/docs/auth-sessions) declared in `auth.scopeTables`. It appears in two forms, which behave differently.

### Indexed — in a landing 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 — with two, the resolver cannot infer which scope to count against. A `pickerLanding` must contain **none**: by definition the multi-record case has no single ID to substitute. Both rules are enforced at config decode.

### Un-indexed — in a dataSource filter

The bare form `$currentUser.assignments.<table>` resolves to the full ID list, so a picker page lists exactly the records the user can reach — and no others:

```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
```

This is what makes the picker safe to expose: the filter is resolved server-side from the session, so a user cannot widen it by editing a request.

## 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 an authenticated visitor the resolver redirects away first, so the page itself is typically empty and never renders.

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

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

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

Earlier drafts of this design forbade a page colliding with `landingPath`. The implementation reverses that: the collision _is_ the mechanism, and the colliding page is required.

## Related Pages

- [Post-Login Landing](/en/docs/auth-post-login) — `landingPath`, `defaultLanding`, and the resolution table.
- [Sessions](/en/docs/auth-sessions) — `scopeTables`, `user_access`, and `$currentUser.activeAssignment`.
- [Roles & RBAC](/en/docs/auth-roles-rbac) — the roles landing rules are declared on.
- [Pages](/en/docs/pages-overview) — page `access` and `dataSource.filter`.
