Vue
permdock/vue maps the snapshot-backed provider, hook and guard from the React adapter onto a Vue plugin, composables and a component.
Purpose
Same model as React: the server serialises permdock.snapshot(), the client evaluates portable grants locally with the core evaluator, and closure grants go to the batched decision endpoint. permdock/vue exposes that through Vue's own primitives: an app plugin for installation, composables returning refs, and a guard component. No policy or server module is imported.
API
// main.ts
import { createApp } from "vue";
import { permdockPlugin } from "permdock/vue";
createApp(App)
.use(permdockPlugin, { snapshot, endpoint: "/api/permdock" })
.mount("#app");<script setup lang="ts">
import { usePermDock, usePermission } from "permdock/vue";
import { permissions } from "@/permissions";
const props = defineProps<{ post: Post }>();
const permdock = usePermDock(); // can / decide / status / invalidate
const { allowed, status } = usePermission(
permissions.post.update,
() => props.post,
); // refs; refetch when post.id changes
</script>
<template>
<Protected :permission="permissions.post.update" :data="post">
<EditButton />
<template #pending><Skeleton /></template>
<template #fallback><Locked /></template>
</Protected>
</template>| Export | Role |
|---|---|
permdockPlugin | app.use(permdockPlugin, options) with snapshot, endpoint (false denies endpoint-only checks with server-only), snapshotUrl, approvals, tenant, fetch, headers, maxAge and verifier. tenant, headers and verifier may be a ref or getter: headers is read on every request, and a changed tenant calls refresh({ tenant }). Provides the store through provide/inject. snapshot may be a ref or getter (the store re-hydrates when it changes) or a promise (the store is pending until it settles; await the same promise in an async setup under <Suspense>). |
usePermDock | Returns the snapshot-backed instance with reactive status. |
usePermission | Accepts the reference and the resource as a MaybeRefOrGetter. Pass a getter or ref so the answer tracks id changes; a plain object is read once, because Vue cannot observe replacement of a plain argument. Returns allowed, status, decision as refs. |
Protected | Component with permission, data and optional tenant props and default, pending, fallback slots. The default slot receives the granted Decision. |
usePermissions, useFilter | Composables mirroring the React hooks: several references against one getter, and filter over a getter of rows; results are computed refs. |
useTenant, useMemberships, useRoles, useAssignableRoles, useAssignablePermissions | Composables returning refs (tenant, tenants, memberships, roles, assignable, the assignable Permission[]) plus switchTo; the same semantics as React (UI, tenancy). |
useApproval, useSubject | Composables for the approval-required flow and the snapshot's subject summary (simulated included). |
For Nuxt, createPermDockUnplugin.vite() runs collect (unplugin). The snapshot arrives from a Nitro route built with the server kernel; the client uses this Vue adapter. There is no permdock/nuxt entry and no Nuxt-specific composable. Gating is done with composables and Protected; there is no v-protected directive.
PermissionBoundary
PermissionBoundary catches PermDockDeniedError and PermDockApprovalRequiredError thrown below it (for example by permdock.assert in a render function) through onErrorCaptured, and renders the denied slot, or the approval slot for an approval request. Both slots receive { outcome, permission, token?, retry }; usePermissionBoundary() returns the same state inside them. Any other error propagates to the next handler.
h(PermissionBoundary, null, {
default: () => h(PublishPanel),
denied: ({ permission }) => h("p", `You cannot ${permission}.`),
});Request lifecycle
- The server creates a request-scoped
PermDock, callssnapshot()and embeds the JSON in the page or a session endpoint. permdockPluginvalidates the snapshot and creates one client store per app instance (SSR-safe: no module-level singleton).usePermissioncomputes the cache key fromreference.keyplus the resourceid. Portable grants answer synchronously; non-portable grants enqueue a batched AuthZENevaluationsrequest toendpoint.- Answers are reactive;
invalidate(permissions.post)drops cached answers under the namespace and pending components re-request.
During SSR (Vite SSR or Nuxt) the store is created per request and hydrated on the client from the same snapshot, so the first client render matches the server (the first-render mismatch permix had in its Vue adapter came from subscribing inside an effect instead of during setup).
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: allowed is false, decision explains why, and Protected renders the fallback slot with the Decision.
Example app
apps/examples/vue: Vue 3 with a snapshot from snapshotFor(policy, memberUser), permdockPlugin, Protected on a portable ownership check, a usePermission delete button and a PermissionBoundary around a panel that calls assert. Vite serves http://127.0.0.1:3481/. The page shows edit, ask to delete and no post.publish.
Related standards
- AuthZEN, Problem Details, Standard Schema.
- Concepts: snapshots, decisions.
Last updated on
React Native
permdock/react-native adds a persisted snapshot so Expo Router Stack.Protected and Tabs.Protected guards answer synchronously on the first frame and revalidate in the background.
Svelte
permdock/svelte maps the snapshot-backed provider, hook and guard onto Svelte context, a readable permission store and a component.