Skip to main content
View as Markdown

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.

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.

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). 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:

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:

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.

auth:
  landingPath: /portal
  # ...

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

Full Example

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.

  • Roles & RBAC — where defaultLanding / pickerLanding are defined on roles.
  • SessionsscopeTables, user_access, and $currentUser.activeAssignment.
  • Pages — page path, access, and dataSource.filter.