# Svelte

Source: https://permdock.com/docs/adapters/svelte

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

## Purpose [#purpose]

Same model as [React](/docs/adapters/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 [#api]

```svelte
<!-- +layout.svelte -->
<script lang="ts">
  import { setPermDock } from 'permdock/svelte'
  let { data, children } = $props()
  setPermDock({ snapshot: data.snapshot, endpoint: '/api/permdock' })
</script>
{@render children()}
```

```svelte
<!-- 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>
```

| Export | Role |
| --- | --- |
| `setPermDock` | Called 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. |
| `getPermDock` | Reads the store from context: `can`, `decide`, `status`, `invalidate`, `refresh`. |
| `permission` | Returns 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. |
| `Protected` | Component with `permission`, `data` and optional `tenant` props and `children`, `pending`, `fallback` snippets. |
| `permissions`, `filtered` | Store 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`, `assignablePermissions` | Readable 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](/docs/concepts/ui), [tenancy](/docs/concepts/tenancy)). |
| `approval`, `subject` | A 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](/docs/adapters/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]

`<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.

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

## Request lifecycle [#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 [#what-it-validates]

The [shared adapter contract](/docs/adapters) 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 [#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`.

## Related standards [#related-standards]

* [AuthZEN](/docs/standards/authzen), [Problem Details](/docs/standards/problem-details), [Standard Schema](/docs/standards/standard-schema).
* Concepts: [snapshots](/docs/concepts/snapshots), [decisions](/docs/concepts/decisions).
