PermDock
Adapters

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>
ExportRole
permdockPluginapp.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>).
usePermDockReturns the snapshot-backed instance with reactive status.
usePermissionAccepts 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.
ProtectedComponent with permission, data and optional tenant props and default, pending, fallback slots. The default slot receives the granted Decision.
usePermissions, useFilterComposables mirroring the React hooks: several references against one getter, and filter over a getter of rows; results are computed refs.
useTenant, useMemberships, useRoles, useAssignableRoles, useAssignablePermissionsComposables returning refs (tenant, tenants, memberships, roles, assignable, the assignable Permission[]) plus switchTo; the same semantics as React (UI, tenancy).
useApproval, useSubjectComposables 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

  1. The server creates a request-scoped PermDock, calls snapshot() and embeds the JSON in the page or a session endpoint.
  2. permdockPlugin validates the snapshot and creates one client store per app instance (SSR-safe: no module-level singleton).
  3. usePermission computes the cache key from reference.key plus the resource id. Portable grants answer synchronously; non-portable grants enqueue a batched AuthZEN evaluations request to endpoint.
  4. 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.

Last updated on

On this page