# Permission-based routing
The router includes a flexible permission system for role-based access control.
## 1. Configure the permission system
In `main.js`, before mounting:
```javascript
import {
configurePermissions,
setCurrentUser
} from '@keenmate/svelte-spa-router/helpers/permissions'
configurePermissions({
checkPermissions: (user, requirements) => {
if (!user) return false
if (!requirements) return true
// Check if user has any of the required permissions
if (requirements.any) {
return requirements.any.some(perm => user.permissions.includes(perm))
}
// Check if user has all required permissions
if (requirements.all) {
return requirements.all.every(perm => user.permissions.includes(perm))
}
return true
},
onUnauthorized: (detail) => {
push('/unauthorized')
}
})
// Push the current user into the permission system. The library keeps an
// internal $state-backed user, so every hasPermission() call site in a
// reactive context (templates, $derived, $effect) re-evaluates automatically
// when you call setCurrentUser() again.
setCurrentUser(null) // logged-out at startup
// Later, on login:
// setCurrentUser({ id: 42, permissions: ['admin.read'] })
// From a websocket permission update:
// setCurrentUser({ ...getCurrentUser(), permissions: newPerms })
// On logout:
// setCurrentUser(null)
```
> **Reactivity:** `hasPermission()` re-evaluates automatically when you call
> `setCurrentUser()` — the function reads from an internal `$state` rune, so
> Svelte's tracker registers the dependency in any reactive context (template
> `{#if}`, `$derived`, `$effect`). No subscription wiring needed on your side.
>
> If you maintain your own reactive user store and prefer to read from it
> directly, pass `getCurrentUser` to `configurePermissions`:
>
> ```js
> configurePermissions({ getCurrentUser: () => myUserState.user, ... })
> ```
>
> Watch out for non-tracked reads (`get(store)`, `localStorage.getItem`, etc.) —
> those won't propagate updates, and your `{#if hasPermission(...)}` blocks
> will appear "broken" (only updating on navigation). The default
> `setCurrentUser`-based path avoids this footgun entirely.
### Re-validating the currently mounted route
`hasPermission()` reactivity covers UI element visibility — the user's menu
and buttons update live when permissions change. It does **not** cover the
case where the user is *sitting on a protected page* when their permissions
are revoked. The router checks route conditions only during navigation, so a
user already on `/admin` who loses admin permission stays on `/admin` until
they navigate away.
To handle this case, call `revalidateCurrentRoute()` after the permission
change:
```javascript
import { revalidateCurrentRoute } from '@keenmate/svelte-spa-router'
import { setCurrentUser, getCurrentUser } from '@keenmate/svelte-spa-router/helpers/permissions'
socket.on('permissions:updated', (newPerms) => {
setCurrentUser({ ...getCurrentUser(), permissions: newPerms })
revalidateCurrentRoute()
})
```
This re-runs the matched route's guards and conditions against the current
location. On success, nothing visible happens — the component keeps its
state (no flicker, no scroll reset, no in-flight form data lost). On
failure, the same unauthorized handling that runs for fresh navigation
fires here too.
If you want to customize the failure path — e.g. show a confirmation dialog
before redirecting, soft-warn the user, log to an audit trail — provide an
`onRevalidationFailure` handler:
```javascript
configurePermissions({
// ... checkPermissions, etc.
onRevalidationFailure: async (detail) => {
const confirmed = await showConfirmDialog(
'Your permissions have changed. Return to the home page?'
)
if (confirmed) {
push('/')
}
// If the user dismisses the dialog, they stay on the current page.
}
})
```
When `onRevalidationFailure` is configured, it fires **instead of** the
standard unauthorized handling for revalidation failures. The
`onConditionsFailed` Router event still fires for consistency with normal
navigation. Pass `onRevalidationFailure: null` to clear and fall back to
standard handling.
Calls to `revalidateCurrentRoute()` within a ~50ms window are coalesced into
a single re-validation pass — safe to call on every websocket message.
## 2. Protect routes with permissions
### Using createProtectedRoute() (recommended)
The most convenient way — no `wrap()` needed!
```javascript
import { createProtectedRoute } from '@keenmate/svelte-spa-router/helpers/permissions'
import { push } from '@keenmate/svelte-spa-router'
const routes = {
'/': Home,
// No wrap() needed! createProtectedRoute() returns ready-to-use wrapped component
'/admin': createProtectedRoute({
component: () => import('./Admin.svelte'),
permissions: { any: ['admin.read', 'admin.write'] },
loadingComponent: Loading,
title: 'Admin Panel',
breadcrumbs: [
{ label: 'Home', path: '/' },
{ label: 'Admin' }
]
}),
// User needs ALL of these permissions
'/settings': createProtectedRoute({
component: () => import('./Settings.svelte'),
permissions: { all: ['settings.read', 'settings.write'] },
title: 'Settings'
}),
// Combine role-based and resource-based authorization
'/document/:id': createProtectedRoute({
component: () => import('./DocumentDetail.svelte'),
// Role-based: User must have 'read' permission
permissions: { any: ['read'] },
// Resource-based: User must have access to THIS specific document
authorizationCallback: async (detail) => {
const documentId = detail.routeParams.id
const hasAccess = await checkDocumentAccess(documentId)
if (!hasAccess) {
// push(route, routeParams, queryString, navigationContext)
await push('/unauthorized', {}, {}, {
resource: 'document',
id: documentId
})
return false
}
return true
},
loadingComponent: Loading,
shouldDisplayLoadingOnRouteLoad: true,
title: 'Document Detail'
}),
'/unauthorized': Unauthorized,
'*': NotFound
}
```
### Using wrap() with createProtectedRouteDefinition() (advanced)
For more control or when combining with other `wrap` options:
```javascript
import { wrap } from '@keenmate/svelte-spa-router/wrap'
import { createProtectedRouteDefinition } from '@keenmate/svelte-spa-router/helpers/permissions'
const routes = {
'/admin': wrap(createProtectedRouteDefinition({
component: () => import('./Admin.svelte'),
permissions: { any: ['admin.read', 'admin.write'] },
loadingComponent: Loading
}))
}
```
## 3. Show/hide UI elements based on permissions
```svelte
```
**Permission requirements:**
- `any: [...]` — User needs at least ONE of these permissions (OR logic)
- `all: [...]` — User needs ALL of these permissions (AND logic)
**Authorization execution order:**
When using both `permissions` and `authorizationCallback`:
1. **Permissions check** (fast, synchronous) — checks user roles/permissions
2. **Authorization callback** (slow, can be async) — checks resource-level access (API calls, database queries, etc.)
This order ensures fast checks happen first, preventing unnecessary API calls when user doesn't have basic permissions.
**Resource-based authorization details:**
The `authorizationCallback` receives a detail object with:
```typescript
{
route: string,
location: string,
params: Record,
query: Record,
routeContext: any,
navigationContext: any
}
```
Perfect for:
- Document access control (check if user can access specific document)
- Resource ownership (check if user owns this resource)
- Dynamic permissions (permissions stored in database)
- API-based authorization (call your backend for access check)
See `example-permissions/` for a complete working example with mock authentication.
## Conditions vs permissions
Both gate access to a route, but they have **different defaults** and **different failure paths**. Reach for the one that matches your situation:
| | `wrap({ conditions: [...] })` | `createProtectedRoute({ permissions: ... })` |
|---|---|---|
| **What it is** | Low-level primitive: any sync/async predicate(s) you want | Opinionated wrapper around conditions, built on the configured permission system |
| **Setup needed** | None — just write the function | Call `configurePermissions({ checkPermissions, getCurrentUser, onUnauthorized })` once at app start |
| **Where the logic lives** | Inline in the condition function | Inside `checkPermissions` (your function), which is reused across every protected route |
| **On failure: UI** | **Empty slot.** The matched component does not mount; nothing renders in its place unless the consumer redirects | The configured `Unauthorized` component mounts (or `onUnauthorized` callback runs, if set), with `unauthorizedBehavior: 'component' \| 'navigate'` controlling which |
| **On failure: event** | `onConditionsFailed` fires with `{ route, location, querystring, params }` | Same event fires (permissions are conditions under the hood); the unauthorized handling runs in addition |
| **When to choose it** | Ad-hoc check that doesn't fit a generic permission model — feature flags, subscription state, ownership of a single resource, custom redirects | Role/permission-based access control where the same `checkPermissions` logic governs many routes and you want a consistent unauthorized UX |
**Common combo:** use `createProtectedRoute` for the role check (gets you the Unauthorized UI) *and* pass extra `conditions` for one-off checks specific to that route. The router runs them in order — permissions first (fast), then your custom conditions.
## Active link highlighting
```svelte
Books
```