PermDock
Adapters

Svelte

permdock/svelte maps the snapshot-backed provider, hook and guard onto Svelte context, a readable permission store and a component.

Purpose

Same model as React: snapshot from the server, local evaluation of portable grants, batched decision endpoint for closure grants. permdock/svelte uses Svelte's context API to hold one client store per component tree and a readable store per permission question so templates can use the $ prefix. Svelte 5 runes are supported through the same store objects rather than a separate rune-based API. No policy or server module is imported.

API

<!-- +layout.svelte -->
<script lang="ts">
  import { setPermDock } from 'permdock/svelte'
  let { data, children } = $props()
  setPermDock({ snapshot: data.snapshot, endpoint: '/api/permdock' })
</script>
{@render children()}
<!-- PostActions.svelte -->
<script lang="ts">
  import { getPermDock, permission, Protected } from 'permdock/svelte'
  import { permissions } from '$lib/permissions'
  let { post } = $props()
  const permdock = getPermDock()                                  // can / decide / status / invalidate
  const canEdit = permission(permissions.post.update, () => post) // readable store: { allowed, status, decision }
</script>

{#if $canEdit.allowed}<EditButton />{/if}

<Protected permission={permissions.post.update} data={post}>
  <EditButton />
  {#snippet pending()}<Skeleton />{/snippet}
  {#snippet fallback(decision)}<Locked reason={decision.denials[0]?.reason} />{/snippet}
</Protected>
ExportRole
setPermDockCalled once in a layout. Validates the snapshot, creates the client store and sets it in Svelte context. Options: snapshot, endpoint (false denies endpoint-only checks with server-only), snapshotUrl, approvals, tenant, fetch, headers, maxAge, verifier. headers may be a getter, read on every request; a tenant getter (() => page.params.org) calls refresh({ tenant }) when it changes. snapshot may be a getter (() => data.snapshot) or a readable store, which re-hydrates the store on change, or a promise, which keeps it pending until it settles (render the same promise with {#await}). Store factories also recompute when a rune read inside their data, rows or options getter changes.
getPermDockReads the store from context: can, decide, status, invalidate, refresh.
permissionReturns a readable store for one reference and, for instance actions, a getter for the resource. Emits allowed, status, decision; re-evaluates when the resource id changes.
ProtectedComponent with permission, data and optional tenant props and children, pending, fallback snippets.
permissions, filteredStore factories mirroring usePermissions and useFilter: a readable store of one entry per reference, and a derived store of the rows the subject may act on.
tenant, memberships, roles, assignable, assignablePermissionsReadable stores from context for the active tenant (with switchTo on the store object), the membership list, roles held in a tenant, roles the subject may hand out and the custom-role ceiling permissions it may hand out (assignablePermissions({ tenant? })) (UI, tenancy).
approval, subjectA store factory for the approval-required flow and a readable store of the snapshot's subject summary (simulated included).

In SvelteKit the snapshot is loaded in +layout.server.ts from a request-scoped PermDock, and the decision endpoint is a +server.ts route built on the server kernel; there is no permdock/sveltekit entry. The client store is component-scoped through context, so load functions and hooks use the server instance, not the store.

PermissionBoundary

<PermissionBoundary> wraps <svelte:boundary>, so it needs Svelte 5.3 or later. It catches PermDockDeniedError and PermDockApprovalRequiredError thrown while its children render and shows the denied snippet, or the approval snippet for an approval request. Each snippet receives { outcome, permission, token?, retry }. Any other error is rethrown to the next boundary.

<PermissionBoundary>
  <PublishPanel />
  {#snippet denied(refused)}<p>You cannot {refused.permission}.</p>{/snippet}
</PermissionBoundary>

Request lifecycle

  1. +layout.server.ts creates the request-scoped instance and returns permdock.snapshot() as page data.
  2. setPermDock runs during component initialisation (not in an effect), so the first server render and the first client render already see the snapshot.
  3. permission(...) derives its answer synchronously for portable grants; non-portable grants enqueue a batched AuthZEN evaluations request to endpoint and emit pending until the answer arrives.
  4. Answers are cached by reference.key plus resource id. invalidate(permissions.post) drops that namespace and subscribed stores refetch.
  5. When page data changes (an org switch, invalidateAll()), a snapshot: () => data.snapshot getter makes the store replace its snapshot and drop cached answers; a plain value is read once at initialisation.

What it validates

The shared adapter contract applies: an invalid snapshot puts the store in server-only mode, the arity of the permission check (collection versus instance actions) is a type error, and resource data is never validated on the client because the decision endpoint validates posted data at the boundary and the API re-checks every mutation. Denials follow the React adapter: $store.allowed is false, $store.decision explains why, and Protected renders the fallback snippet with the Decision.

Example app

apps/examples/svelte: Svelte with a snapshot from snapshotFor(policy, memberUser), setPermDock, Protected, a permission store for the delete button and a PermissionBoundary around a panel that calls assert. Vite serves http://127.0.0.1:3482/. The page shows edit, ask to delete and no post.publish.

Last updated on

On this page