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. |
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). 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
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
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 — where
defaultLanding/pickerLandingare defined on roles. - Sessions —
scopeTables,user_access, and$currentUser.activeAssignment. - Pages — page
path,access, anddataSource.filter.