@vertz/ui/auth module provides UI-advisory access checks. The server computes an access set (what the user can do), embeds it in the JWT, and the client uses it to show/hide UI elements without network requests.
This is advisory only. The server always re-validates before mutations. Client-side can() controls UI visibility, not authorization.
Setup
With AuthProvider (recommended)
If your app usesAuthProvider, enable access control with a single prop:
AccessContext.Provider, fetches the access set after authentication, clears it on sign out, and hydrates from SSR.
Standalone with createAccessProvider()
If you’re not using AuthProvider, use createAccessProvider() to bootstrap the access context manually. This is useful when you have your own auth solution or a custom session management layer.
AccessContextValue with two signal properties:
| Property | Type | Description |
|---|---|---|
accessSet | Signal<AccessSet | null> | The decoded access set, or null if not yet loaded |
loading | Signal<boolean> | true while the access set is being loaded |
createAccessProvider() checks for window.__VERTZ_ACCESS_SET__. If present (injected during SSR), the access set is immediately available and loading is false — no loading flicker. If not present, loading starts as true until you hydrate it by setting accessValue.accessSet.value directly.
Example — wrap your app manually:
Inject the access set during SSR
On the server, extract the access set from the JWT and inject it as a script tag:createAccessSetScript() produces a <script> tag that sets window.__VERTZ_ACCESS_SET__. It escapes all characters that could enable XSS via JSON injection. Pass a CSP nonce if your application uses nonce-based Content Security Policy:
Checking entitlements with can()
can() checks if the current user has a specific entitlement. Call it in the component body (like query() or form()):
Return shape
can() returns an AccessCheck with reactive properties:
| Property | Type | Description |
|---|---|---|
allowed | boolean | Whether the entitlement is granted |
reasons | DenialReason[] | All denial reasons, ordered by actionability |
reason | DenialReason | undefined | The most actionable denial reason |
meta | DenialMeta | undefined | Metadata (usage limits, required plans, etc.) |
loading | boolean | true while the access set is loading |
.value — you use them like plain values in JSX and event handlers.
Denial reasons
Reasons are ordered from most actionable (things the user can fix) to least:| Reason | Meaning | User action |
|---|---|---|
plan_required | Entitlement requires a higher plan | Upgrade |
role_required | User lacks the required role | Request access |
limit_reached | Usage limit exceeded for the billing period | Wait or upgrade |
flag_disabled | Feature flag is off | None |
hierarchy_denied | No access path through the resource hierarchy | Request access |
step_up_required | Needs recent MFA verification | Re-authenticate |
not_authenticated | No user session | Sign in |
DenialMeta structure
When can() returns a denial, the meta property contains structured metadata about why. The DenialMeta type has four optional fields — each is populated only when relevant to the denial reason.
| Field | Present when | Description |
|---|---|---|
requiredPlans | reason === 'plan_required' | Plans that include this entitlement (e.g., ['pro', 'enterprise']) |
requiredRoles | reason === 'role_required' | Roles that grant this entitlement (e.g., ['admin', 'editor']) |
limit | reason === 'limit_reached' | Current usage state: max (quota), consumed (used), remaining (available) |
fvaMaxAge | reason === 'step_up_required' | Max seconds since last MFA verification |
meta to build contextual UI messages:
Usage limits in meta
When an entitlement has a usage limit,meta.limit contains the current state:
Entity-scoped checks
For entity lists where items have different permissions, the server can pre-compute access metadata per entity (see server-side entity access). Pass the entity as the second argument tocan():
can() checks project.__access['project:delete'] first. If the entity doesn’t have __access metadata, it falls back to the global access set.
AccessGate
AccessGate prevents rendering children until the access set is loaded. Use it to avoid flicker on initial render:
window.__VERTZ_ACCESS_SET__), the gate opens immediately with no loading flash.
When no AccessContext.Provider is present, AccessGate renders children directly (fail-open for UI).
Compiler integration
can is registered in the signal API registry, which means the compiler auto-unwraps signal properties. You write:
.allowed access to read from the underlying signal. You never need .value in JSX or event handlers.
Without a provider
Ifcan() is called without an AccessContext.Provider in the tree, it returns a fail-secure fallback where allowed is false and reason is 'not_authenticated'. In development, a console warning is logged.
Next steps
Authentication
AuthProvider, useAuth(), sign-in forms, and token refresh.
Server-Side Access Control
Configure hierarchy, roles, plans, and usage limits.
Code Generation
Type-safe entitlements and RLS policies from defineAccess().
Entities
Use access rules to protect entity operations.