Solid
permdock/solid maps the snapshot-backed provider, hook and guard onto Solid context, signal accessors and a component.
Purpose
Same model as React: snapshot from the server, local evaluation of portable grants, batched decision endpoint for closure grants. permdock/solid exposes it through a context provider, accessor-returning hooks so answers participate in Solid's fine-grained reactivity, and a guard component. No policy or server module is imported.
API
import {
PermDockProvider,
usePermDock,
usePermission,
Protected,
} from "permdock/solid";
import { permissions } from "~/permissions";
<PermDockProvider snapshot={snapshot} endpoint="/api/permdock">
<App />
</PermDockProvider>;
function PostActions(props: { post: Post }) {
const permdock = usePermDock(); // can / decide / status / invalidate
const canEdit = usePermission(permissions.post.update, () => props.post); // accessor: () => { allowed, status, decision }
return (
<Protected
permission={permissions.post.update}
data={props.post}
pending={<Skeleton />}
fallback={<Locked />}
>
<EditButton />
</Protected>
);
}| Export | Role |
|---|---|
PermDockProvider | Creates the client store once, validates the snapshot, provides it through Solid context. Props: snapshot, endpoint (false denies endpoint-only checks with server-only), snapshotUrl, approvals, tenant, fetch, headers, maxAge, verifier. headers, fetch and verifier are read on every request, and a changed tenant calls refresh({ tenant }). snapshot may be an accessor (a createResource result included: the store stays pending while it is undefined, then re-hydrates on every change) or a promise. <Protected> follows a changed permission and accepts JSX children. |
usePermDock | Returns the store: can, decide, status (accessor), invalidate, refresh. |
usePermission | Takes the reference and an accessor for the resource; returns an accessor of allowed, status, decision. Tracks the resource id, so a new post re-evaluates. |
Protected | Guard component with permission, data, optional tenant, pending, fallback props; children may be a function receiving the granted Decision. |
usePermissions, useFilter | Accessor-returning counterparts of the React hooks: several references against one accessor, and filter over an accessor of rows. |
useTenant, useMemberships, useRoles, useAssignableRoles, useAssignablePermissions | Accessors for the active tenant (tenant(), tenants(), switchTo), the membership list, roles held in a tenant, roles the subject may hand out and the custom-role ceiling permissions it may hand out (UI, tenancy). |
useApproval, useSubject | The approval-required flow as an accessor of { state, token } with request(), and the snapshot's subject summary (simulated included). |
The hooks keep the React names with accessor-returning semantics, and usePermission returns one accessor rather than a tuple. For SolidStart the snapshot is loaded in a server function from a request-scoped PermDock and the decision endpoint is an API route built on the server kernel; there is no permdock/solid-start entry.
PermissionBoundary
PermissionBoundary wraps Solid's ErrorBoundary. It catches PermDockDeniedError and PermDockApprovalRequiredError thrown below it and renders denied, or approval for an approval request. Each is JSX or a function of { outcome, permission, token?, retry }, and usePermissionBoundary() returns the same state inside the fallback. Any other error is rethrown to the next ErrorBoundary.
<PermissionBoundary
denied={(refused) => <p>You cannot {refused.permission}.</p>}
>
<PublishPanel />
</PermissionBoundary>Request lifecycle
- The server creates the request-scoped instance and returns
permdock.snapshot()with the page (or from a session endpoint). PermDockProvidervalidates the snapshot and builds one store per provider instance. Hooks subscribe during render, and a snapshot accessor is forwarded to the store in a computation (createComputed), not an effect: effects under a suspended<Suspense>are deferred during hydration, so acreateAsyncsnapshot that resolved while a sibling was still streaming could leave the storepending. permix's Solid adapter subscribed in a deferred effect and missed the first render.usePermissionderives portable answers synchronously in a memo keyed byreference.keyplus resourceid. Non-portable grants go through the shared store, which batches them into one AuthZENevaluationsrequest toendpoint.invalidate(permissions.post)drops cached answers under the namespace and refetches active ones.
Accessors matter for correctness here: usePermission(permissions.post.update, () => props.post) re-evaluates when props.post.id changes, whereas passing props.post directly would read the prop once and freeze the answer (the refetch bug Kilpi's useAuthorize had). The adapter accepts only an accessor for instance actions, so the type system prevents the mistake.
For SSR with renderToStream, the provider serialises nothing extra: the snapshot is already page data, and the client store is created from the same JSON, so hydration produces identical answers.
In SolidStart, mount the provider inside the <Suspense> boundary once the snapshot has resolved. Streaming SSR then waits for the snapshot inside the boundary and hydration builds the store from it. A createAsync accessor handed to a provider above every boundary can still resolve without reaching the store under concurrent streaming requests; the recipe below avoids it.
const snapshot = createAsync(() => getSnapshot(props.params.org));
return (
<Suspense fallback={<NavSkeleton />}>
<Show when={snapshot()}>
{(current) => (
<PermDockProvider snapshot={current}>{props.children}</PermDockProvider>
)}
</Show>
</Suspense>
);getSnapshot is a query whose body starts with 'use server' and returns snapshotFor(...) for the request's session; revalidate(getSnapshot.key) after a role or plan change re-hydrates the same store through the current accessor.
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: the accessor yields allowed: false with the decision, and Protected renders fallback, passing the Decision to a function fallback.
Example app
apps/examples/solid: Solid with a snapshot from snapshotFor(policy, memberUser), PermDockProvider, Protected on a portable ownership check, a usePermission label and a PermissionBoundary around a panel that calls assert. Vite serves http://127.0.0.1:3483/. The page shows edit, ask to delete and no post.publish.
Related standards
- AuthZEN, Problem Details, Standard Schema.
- Concepts: snapshots, decisions.
Last updated on
Svelte
permdock/svelte maps the snapshot-backed provider, hook and guard onto Svelte context, a readable permission store and a component.
Next.js
permdock/next wires one explicit server factory into Server Components, Server Actions, Route Handlers and the client, built for Next.js 16.3 Cache Components and Instant Navigations.