# Solid

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

permdock/solid maps the snapshot-backed provider, hook and guard onto Solid context, signal accessors 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/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 [#api]

```tsx
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](/docs/concepts/ui), [tenancy](/docs/concepts/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](/docs/adapters/server-kernel); there is no `permdock/solid-start` entry.

### PermissionBoundary [#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`.

```tsx
<PermissionBoundary
  denied={(refused) => <p>You cannot {refused.permission}.</p>}
>
  <PublishPanel />
</PermissionBoundary>
```

## Request lifecycle [#request-lifecycle]

1. The server creates the request-scoped instance and returns `permdock.snapshot()` with the page (or from a session endpoint).
2. `PermDockProvider` validates 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 a `createAsync` snapshot that resolved while a sibling was still streaming could leave the store `pending`. permix's Solid adapter subscribed in a deferred effect and missed the first render.
3. `usePermission` derives portable answers synchronously in a memo keyed by `reference.key` plus resource `id`. Non-portable grants go through the shared store, which batches them into one AuthZEN `evaluations` request to `endpoint`.
4. `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.

```tsx
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 [#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: the accessor yields `allowed: false` with the `decision`, and `Protected` renders `fallback`, passing the `Decision` to a function `fallback`.

## Example app [#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 [#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).
