# Adapters

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

One core, one Fetch-first server kernel, and thin typed adapters for UI frameworks, HTTP servers, RPC layers, agent runtimes, the decision plane, databases and auth providers.

PermDock ships as one npm package (`permdock`) with subpath exports. Every subpath is a thin adapter over the same core: the typed permission definition, the policy, and the immutable, request-scoped `PermDock` instance created by `createPermDock`. Adapters never re-implement evaluation. They resolve the subject from the framework's request or session, build the instance once per request, and translate the resulting `Decision` into the framework's own vocabulary: an HTTP 403 Problem Details body, a Next.js `redirect()`, an MCP refusal, an AI SDK approval verdict, or a boolean for a router guard.

## Why thin adapters over one core [#why-thin-adapters-over-one-core]

* One evaluation path. `can`, `decide`, `assert`, `filter`, `where`, `simulate` and `snapshot` live on the core instance; adapters only add framework glue. Fixes and semantics (deny overrides allow, fail-closed, boundary validation) apply everywhere at once.
* One name everywhere. Every server and agent adapter exports `createPermDock`; the import path names the framework (`permdock/hono`, `permdock/next`), the identifier never does. See [naming](/docs/getting-started/naming).
* A Fetch-first kernel. `permdock/server` speaks `Request` / `Response`. Hono, Express, Fastify, Elysia, Nest and Node adapters wrap the kernel with a few lines of typed glue instead of five separate implementations (permix adapters each copy request isolation; see [landscape](/docs/research/landscape)).
* One package, subpath exports. Every adapter ships inside `permdock`, so an app (or an agent wiring one) needs one install line, core and adapters are always the same version, and there is no "adapter X requires core Y" matrix. Framework SDKs are optional peers, and `publint` and `arethetypeswrong` keep the `exports` map honest. Scoped per-adapter packages were rejected for install friction and version skew. The test runners are a subpath too: `permdock/testing` is imported only from test files, and Vitest is an optional peer. The CLI ships in the same package as the `permdock` binary, `permdock/cli`, `permdock/unplugin` and `permdock/next/plugin`. No runtime entry imports CLI code, so its parsers never land in an application bundle; `oxc-parser` is the one CLI dependency, and the Postgres (WASM) parser, `pg` and `unplugin` are optional peers loaded only by the commands that need them.
* Wire-format readers are runtime entries. `permdock/catalog` reads `permissions.catalog.json` (`parseCatalog`, the `Catalog*` types, `rowConditionKeys`) for a package that consumes the catalog at run time, such as a session library or an admin UI, without loading the CLI. Without it, every reader copies the types, and the copies drift from `schemas/catalog-v1.json`. The reader and `permdock catalog --format schema` share one schema constant, so the runtime check and the published schema cannot disagree. It runs on every WinterTC runtime and adds no dependency.
* Client entries are server-free. `permdock/react`, `permdock/react-native`, `permdock/vue`, `permdock/svelte`, `permdock/solid` and `permdock/webmcp` import only the definition and a snapshot. Policies and subject resolvers never enter a client bundle.
* Size stays small by construction: ESM-only, runtime entries that import only one types-only spec package (`@standard-schema/spec`), and CLI code no runtime entry reaches. Each entry's gzip size is measured in `tests/bundle` against a recorded regression baseline.

## When a tool gets an entry [#when-a-tool-gets-an-entry]

An adapter entry exists only where in-process code has to occupy a runtime slot: a `protect` middleware position, a `canUseTool` callback, an ORM query builder, a React context, an RSC `cache`. Everything else is reached through a wire format or a documented recipe: standard OpenAPI `security` plus `x-permdock-*` delivered as an [Overlay](/docs/standards/openapi-overlay), AuthZEN, Standard Schema, JWT and JWKS, OCSF, Problem Details. PermDock composes with spec producers, SDK generators, docs UIs, flag SDKs and sinks and does not wrap them, so there is no `permdock/hey-api`, `permdock/scalar` or other per-vendor entry, and a new one needs a design decision first. Any consumer that reads the standard fields works on day one, and PermDock carries no maintenance burden for other tools' release cycles.

The same rule applies to PermDock's own HTTP adapters. A framework whose handlers already receive a Fetch `Request` (SvelteKit, Nitro, Remix, Astro server routes) uses `permdock/server` through a recipe rather than getting a new subpath. A new server adapter must either convert a non-Fetch request object (Express, Fastify, Node, Nest) or expose a runtime-specific hook (Next.js, tRPC, oRPC, the agent runtimes). Hono and Elysia remain entries: they are thin wrappers that pass the native `Request` to the kernel, and they show that a Fetch framework is a one-file wrapper. `permdock/better-supabase` is an entry for the same reason as `permdock/better-auth`: better-supabase's config, buckets, topics and API key block take runtime values (an `AuthorizationProvider`, access policies, a claim), and the entry builds them from the manifest and catalog, so `permdock/supabase` stays plain Supabase.

## Adapter matrix [#adapter-matrix]

An adapter page carries a `Status: planned` line only when the adapter is not built yet. Example apps live under `apps/examples/<name>`.

Nuxt, Astro, React Router, TanStack Start and Effect are not rows on this matrix. They collect through [`createPermDockUnplugin`](/docs/cli/unplugin) and run on the UI / HTTP adapters.

| Adapter | Import path | What `createPermDock` (or the entry) returns | Example app | Related standards |
| --- | --- | --- | --- | --- |
| [react](/docs/adapters/react) | `permdock/react` | Direct exports, no factory: `PermDockProvider`, `usePermDock`, `usePermission`, `Protected` | `apps/examples/react-vite`, `apps/examples/react-router` | [AuthZEN](/docs/standards/authzen), [Problem Details](/docs/standards/problem-details) |
| [react-native](/docs/adapters/react-native) | `permdock/react-native` | Same exports as React plus a `storage` option for a persisted snapshot | `apps/examples/expo` | [AuthZEN](/docs/standards/authzen) |
| [next](/docs/adapters/next) | `permdock/next` | `getPermDock`, `getPermission`, `requireAccess`, `PermDockProvider`, `permdockHandler`; `PermissionBoundary` in `permdock/next/client`; OpenAPI via next-openapi-gen plus the PermDock Overlay | `apps/examples/next`, `apps/examples/next-better-supabase` | [AuthZEN](/docs/standards/authzen), [Problem Details](/docs/standards/problem-details), [OpenAPI Overlay](/docs/standards/openapi-overlay) |
| [vue](/docs/adapters/vue) | `permdock/vue` | Plugin install plus `usePermDock` / `usePermission` composables, `Protected` and `PermissionBoundary` | `apps/examples/vue` | [AuthZEN](/docs/standards/authzen) |
| [svelte](/docs/adapters/svelte) | `permdock/svelte` | Context setter plus `permission` store, `Protected` and `PermissionBoundary` | `apps/examples/svelte` | [AuthZEN](/docs/standards/authzen) |
| [solid](/docs/adapters/solid) | `permdock/solid` | `PermDockProvider`, `usePermDock`, `usePermission` signal accessor, `Protected`, `PermissionBoundary` | `apps/examples/solid` | [AuthZEN](/docs/standards/authzen) |
| [server-kernel](/docs/adapters/server-kernel) | `permdock/server` | Fetch kernel: `permdock(request)`, `protect`, `problem`, `openapi` hook contract; `createServerKernel` for out-of-tree adapters | — | [Problem Details](/docs/standards/problem-details), [OpenAPI 3.2](/docs/standards/openapi), [Web Bot Auth](/docs/standards/web-bot-auth) |
| [hono](/docs/adapters/hono) | `permdock/hono` | `permdock` middleware and `protect` route guard | `apps/examples/hono` | [Problem Details](/docs/standards/problem-details), [OpenAPI 3.2](/docs/standards/openapi), [Web Bot Auth](/docs/standards/web-bot-auth) |
| [express](/docs/adapters/express) | `permdock/express` | `permdock` middleware and `protect` middleware | `apps/examples/express` | [Problem Details](/docs/standards/problem-details), [OpenAPI 3.2](/docs/standards/openapi) |
| [fastify](/docs/adapters/fastify) | `permdock/fastify` | `permdock` plugin (decorates `request.permdock`) and `protect` preHandler | `apps/examples/fastify` | [Problem Details](/docs/standards/problem-details), [OpenAPI 3.2](/docs/standards/openapi) |
| [elysia](/docs/adapters/elysia) | `permdock/elysia` | `permdock` plugin (derives `permdock` into context) and `protect` hook | `apps/examples/elysia` | [Problem Details](/docs/standards/problem-details), [OpenAPI 3.2](/docs/standards/openapi) |
| [nest](/docs/adapters/nest) | `permdock/nest` | `PermDockModule`, `PermDockGuard`, `Protect`, `InjectPermDock` | `apps/examples/nest` | [Problem Details](/docs/standards/problem-details), [OpenAPI 3.2](/docs/standards/openapi) |
| [node](/docs/adapters/node) | `permdock/node` | `permdock`, `protect`, `send` over IncomingMessage; shared `toRequest` / `fromResponse` | — | [Problem Details](/docs/standards/problem-details) |
| [terminal](/docs/adapters/terminal) | `permdock/terminal` | `permdock`, `protect`, `filterCommands`, `format` for your own CLI (commander, citty, oclif, yargs, Ink) | `apps/examples/terminal` | [Problem Details](/docs/standards/problem-details), [FAPI 2.0](/docs/standards/fapi-2) |
| [trpc](/docs/adapters/trpc) | `permdock/trpc` | `permdock` context helper and `protect` middleware; `trpc-to-openapi` `protect` hook | `apps/examples/trpc` | [OpenAPI 3.2](/docs/standards/openapi), [Problem Details](/docs/standards/problem-details) |
| [orpc](/docs/adapters/orpc) | `permdock/orpc` | `permdock` middleware and `protect` middleware; `openapi()` OpenAPI metadata | `apps/examples/orpc` | [OpenAPI 3.2](/docs/standards/openapi), [Problem Details](/docs/standards/problem-details) |
| [mcp](/docs/adapters/mcp) | `permdock/mcp` | `protectServer` (hosted through `mcp-handler`, the SDK middleware or `McpAgent`) | `apps/examples/mcp-server` | [MCP authorization](/docs/standards/mcp-authorization), [OAuth agent delegation](/docs/standards/oauth-agent-delegation) |
| [ai-sdk](/docs/adapters/ai-sdk) | `permdock/ai-sdk` | `toolApproval`, `capabilityMiddleware`, `needsApproval` | `apps/examples/ai-sdk-agent` | [OAuth agent delegation](/docs/standards/oauth-agent-delegation) |
| [claude-agent](/docs/adapters/claude-agent) | `permdock/claude-agent` | `canUseTool`, `permissionRequestHook` | `apps/examples/claude-agent` | [OAuth agent delegation](/docs/standards/oauth-agent-delegation) |
| [eve](/docs/adapters/eve) | `permdock/eve` | `approval` (request and response policies for `defineTool`), `approvalFor`, `permdock` | `apps/examples/eve-agent` (also the Marketplace template) | [OAuth agent delegation](/docs/standards/oauth-agent-delegation) |
| [openai](/docs/adapters/openai) | `permdock/openai` | `needsApproval`, `guardTools`, `resolveInterruptions`, `permdock` | `apps/examples/openai-agent` | [MCP authorization](/docs/standards/mcp-authorization) (hosted MCP tools) |
| [webmcp](/docs/adapters/webmcp) | `permdock/webmcp` | `registerTools` (client entry, no factory) | `apps/examples/webmcp` | [WebMCP](/docs/standards/webmcp) |
| [a2a](/docs/adapters/a2a) | `permdock/a2a` | `agentCard`, `extendedAgentCard`, `protectSkill` | `apps/examples/a2a-agent` | [A2A](/docs/standards/a2a) |
| [authzen](/docs/adapters/authzen) | `permdock/authzen` | `permdockHandler` serving evaluation, evaluations, search and discovery | `apps/examples/authzen-pdp` | [AuthZEN](/docs/standards/authzen) |
| [approvals](/docs/adapters/approvals) | `permdock/approvals` | No factory: `ApprovalStore` interface, `memoryApprovalStore`, `approvalsHandler`; the `store` option of every agent and HTTP adapter | — (used by `ai-sdk-agent`, `eve-agent`, `openai-agent`, `terminal`) | [Problem Details](/docs/standards/problem-details) |
| [cloud](/docs/adapters/cloud) | `permdock/cloud` | No factory: `cloud({ url, key })` returning `approvals`, `sink`, `snapshots` for PermDock Cloud (`https://app.permdock.com`, `https://api.permdock.com`); never on the decision path | `apps/examples/eve-agent` | [AuthZEN](/docs/standards/authzen), [Shared Signals and CAEP](/docs/standards/shared-signals-caep) |
| [cloud-integrations](/docs/adapters/cloud-integrations) | — (PermDock Cloud connectors) | No entry: the catalog of what the Cloud connects to (trusted issuers, CAEP, SCIM, Vercel, OTLP, OCSF, webhooks, compliance exports, delivery, a read-only MCP server), every one a standard wire format or an existing interface | — | [OpenID Connect](/docs/standards/openid-connect), [Shared Signals and CAEP](/docs/standards/shared-signals-caep), [SCIM](/docs/standards/scim), [MCP authorization](/docs/standards/mcp-authorization) |
| [ssf](/docs/adapters/ssf) | `permdock/ssf` | `receiver` for CAEP Security Event Tokens | — | [Shared Signals and CAEP](/docs/standards/shared-signals-caep) |
| [scim](/docs/adapters/scim) | `permdock/scim` | No factory: `scimHandler` (RFC 7644 `/Users` and `/Groups` receiver), `DirectoryStore` with `memoryDirectoryStore`, `directoryMembershipSource` (a `MembershipSource` over the synced groups); PermDock Cloud relays provisioning to it | `apps/examples/scim` | [SCIM](/docs/standards/scim), [JWT authorization claims](/docs/standards/jwt-authorization-claims) |
| [openapi](/docs/adapters/openapi) | `permdock/openapi` | Security emitter and document importer used by the HTTP and RPC hooks; Overlay output consumed by next-openapi-gen and Redocly, read by Hey API, Orval, Scalar and OpenAPI-to-MCP bridges | — | [OpenAPI 3.2](/docs/standards/openapi), [OpenAPI Overlay](/docs/standards/openapi-overlay) |
| [otel](/docs/adapters/otel) | `permdock/otel` | `instrument` / `withOtel` (`on('decision')` spans and counters) | — (Hono and MCP examples enable `otel`) | [OpenTelemetry GenAI](/docs/standards/watch-list) |
| [drizzle](/docs/adapters/drizzle) | `permdock/drizzle` | `toWhere` condition compiler | `apps/examples/drizzle` | [Postgres RLS](/docs/standards/postgres-rls) |
| [prisma](/docs/adapters/prisma) | `permdock/prisma` | `toWhere` condition compiler | `apps/examples/prisma` | [Postgres RLS](/docs/standards/postgres-rls) |
| [kysely](/docs/adapters/kysely) | `permdock/kysely` | `toWhere` condition compiler | — | [Postgres RLS](/docs/standards/postgres-rls) |
| [rls](/docs/adapters/rls) | `permdock rls` (CLI) | `generate`, `import`, `verify` | `apps/examples/supabase-rls` | [Postgres RLS](/docs/standards/postgres-rls) |
| [jwt](/docs/adapters/jwt) | `permdock/jwt` | `subjectFromJwt`, `createJwtSubjectResolver` (JWKS, RFC 8725 checks, `profile: 'fapi2'`, RFC 9068 `roles` / `groups` / `entitlements` to roles and memberships); `jose` optional peer | — (used by `hono`, `mcp-server`) | [FAPI 2.0](/docs/standards/fapi-2), [OAuth agent delegation](/docs/standards/oauth-agent-delegation), [JWT authorization claims](/docs/standards/jwt-authorization-claims) |
| [supabase](/docs/adapters/supabase) | `permdock/supabase` | Provider: `subjectFromSupabase` (roles, tenant and memberships from hook-injected claims), custom access token hook, `authorize()` RBAC scaffold, `memberOf` compilation to membership tables | `apps/examples/supabase-rls`, `apps/examples/next-better-supabase` | [Postgres RLS](/docs/standards/postgres-rls), [JWT authorization claims](/docs/standards/jwt-authorization-claims) |
| [supabase (middleware)](/docs/adapters/supabase#middleware-permdocksupabasemiddleware) | `permdock/supabase/middleware` | `withPermDock` (a `@supabase/middleware` entry: requires `jwtClaims` from `withClaims`, contributes `ctx.permdock`; `{ protect, data }` short-circuits with Problem Details), `permdockHandler`, `openapi`; `@supabase/middleware` optional peer, `@supabase/server` and `@supabase/ssr` are recipes | `apps/examples/supabase-middleware` | [Problem Details](/docs/standards/problem-details), [AuthZEN](/docs/standards/authzen) |
| [better-supabase](/docs/adapters/better-supabase) | `permdock/better-supabase` | No factory: `authorizationProvider` (better-supabase's `authorization` config), `bucketPolicy` and `topicPolicy`, `apiKeyVerifier`, `apiKeyClaimOptions`, `subjectFromBetterSupabase` | `apps/examples/next-better-supabase` | — |
| [better-auth](/docs/adapters/better-auth) | `permdock/better-auth` | Provider: `subjectFromBetterAuth` (session, organization and team memberships), `betterAuthRoleSource` (dynamic roles as a `RoleSource`), `rolesFromAccessControl` | `apps/examples/better-auth` | — |
| [clerk](/docs/adapters/clerk) | `permdock/clerk` | Provider: `subjectFromClerk` (session, active organization as tenant, memberships; `memberships: 'all'` through the Backend API), custom roles and role sets as a `RoleSource` | `apps/examples/clerk` | — |
| [convex](/docs/adapters/convex) | `permdock/convex` | Provider: Convex identity to subject inside functions | `apps/examples/convex` | — |
| [pdp](/docs/adapters/pdp) | `permdock/pdp` | AuthZEN PEP client that answers checks from a remote PDP, plus `openfga` and `spicedb` presets | `apps/examples/authzen-pdp` | [AuthZEN](/docs/standards/authzen) |
| [catalog](/docs/cli/catalog#reading-a-catalog) | `permdock/catalog` | No factory: `parseCatalog` (validates and freezes a `permissions.catalog.json`), `rowConditionKeys`, the `Catalog*` types | `apps/examples/next-better-supabase` | — |
| [compile](/docs/concepts/extension-interfaces#wherecompiler) | `permdock/compile` | No factory: `compileWhere` (lowers a portable condition for one subject to the `CompiledWhere` tree that `permdock/drizzle`, `prisma` and `kysely` render), `escapeLike`, the `Compiled*` types | — | [Postgres RLS](/docs/standards/postgres-rls) |
| [testing](/docs/adapters/testing) | `permdock/testing` | Policy matrix tests, snapshot fixtures, RLS parity runner, conformance runners | — | [Postgres RLS](/docs/standards/postgres-rls) |

## The shared adapter contract [#the-shared-adapter-contract]

Every adapter page documents Purpose, API, Example app and Related standards, and records only its own differences from the behaviour below, which is common to all of them. Open questions live on the [roadmap](/docs/roadmap).

### Request lifecycle [#request-lifecycle]

1. Subject resolve. The adapter calls the `subject` option you passed to `createPermDock` with the framework's native handle (`c` in Hono, `req` in Express, `authInfo` in MCP, `cookies()` in Next.js). It may return `null` for anonymous callers. Agent adapters also fill `actor` and `delegation` from the runtime's auth info; the subject is never taken from a model-supplied argument. The resolver receives verified material only: a session the framework already validated, or claims verified by `permdock/jwt` or a provider `subjectFrom*` helper. PermDock never authenticates; see [authentication](/docs/concepts/authentication). The same rule covers `principal.tenant` and `principal.memberships`: the adapter's `tenant` option resolves the active tenant from the URL, a route parameter or the provider's active organisation and passes it to core, which accepts it only when a membership matches; a tenant from an unsigned header or a model argument is never used ([tenancy](/docs/concepts/tenancy)).
2. `createPermDock`. Exactly one immutable `PermDock` is built per request (or per RSC render, per tool call, per RPC procedure) and stored where the framework expects request state. Nothing is shared across requests; there is no global `setup()`.
3. Check. Handlers call `can`, `decide`, `assert`, `filter` or `where` on the request-scoped instance, or let `protect(permission, loadData)` do it before the handler runs.
4. Denial surface. The adapter turns a `denied` or `approval-required` `Decision` into the framework's response type and forwards the `on('decision')` event to audit. See [decisions](/docs/concepts/decisions).
5. Pluggable state. Every adapter that can surface `approval-required` accepts a `store` (an [`ApprovalStore`](/docs/adapters/approvals), in-memory by default) and every adapter accepts a `sink` (a [`DecisionSink`](/docs/concepts/audit-and-observability), in-memory by default). Neither is consulted on the `decide` path; [`permdock/cloud`](/docs/adapters/cloud) is one implementation of both, and an application database is another. Every adapter also accepts `memberships` (a [`MembershipSource`](/docs/concepts/extension-interfaces)) and `customRoles` (a `RoleSource`); these two are subject inputs, run once before the instance is built, and are the only options that may influence an outcome. The Cloud implements neither. A [`DirectoryStore`](/docs/adapters/scim) is the write side of the same input: `scimHandler` fills it from an authenticated identity provider (or the Cloud's relay) and `directoryMembershipSource` reads it as a `MembershipSource`; the store lives in the application, never in the Cloud.

### What every adapter validates [#what-every-adapter-validates]

* Permission references are typed; adapters never accept a permission string from the outside. Strings only appear as `.key` and `.scope` on the wire.
* Data that crossed a trust boundary (HTTP body, route params fed into `loadData`, MCP tool arguments, client `refresh` payloads on the decision endpoint) is validated against the resource's Standard Schema when the policy uses `validate: 'boundary'` (the default). Trusted server rows are not re-validated. See [validation](/docs/concepts/validation).
* Snapshots and decision-endpoint requests are validated against the published wire schemas ([wire formats](/docs/concepts/wire-formats)) before they touch the evaluator.
* Server-only entries (`permdock/next`, the HTTP adapters, providers) are marked so that importing them from a client component fails at build time instead of leaking a policy into the browser bundle.

### How denials surface [#how-denials-surface]

| Surface | `denied` | `approval-required` |
| --- | --- | --- |
| HTTP and RPC | `403` with `application/problem+json`: `type`, `title`, `status`, `permission`, `denials`, `alternatives`. For OAuth callers the RFC 6750 / RFC 9470 header rides along: an unverifiable token is `401` `.../unauthenticated` with `WWW-Authenticate: Bearer error="invalid_token"`; `not-delegated` / `no-delegation` add `error="insufficient_scope", scope="…"`; `insufficient-user-authentication` is `401` `.../step-up-required` with `error="insufficient_user_authentication", acr_values="…", max_age=…` ([Problem Details](/docs/standards/problem-details)) | `403` with `type` ending in `/approval-required`, plus `token` |
| Next.js server | `assert` runs layered handlers; the default handler calls `redirect()` or `notFound()` as configured | Same handler chain; the app decides where to send the user |
| React and other UI | `usePermission` returns `allowed: false` with `status`; `Protected` renders `fallback`; a disabled control uses `usePermission` plus `describe(decision)` for the reason | `allowed: false`, `decision.outcome === 'approval-required'` exposed for a "request access" UI; `useApproval` tracks the request |
| MCP | Tool result with `isError: true` and Decision reasons in `structuredContent`; `not-delegated` becomes a `403 insufficient_scope` challenge, `insufficient-user-authentication` a refusal carrying `acr_values` and `max_age`, or an `input_required` URL request when `stepUp` is set | `input_required` URL request when `approval.at` is set and the client takes URL elicitations; otherwise a refusal carrying `token` |
| AI SDK and Claude Agent SDK | `denied` with reason and alternatives | `user-approval` or `needsApproval`; the Claude hook returns an ask verdict |
| Eve and OpenAI Agents SDK | Eve `{ type: 'denied', reason }`; OpenAI `state.reject(i, { message })` | Eve `"user-approval"` parks the session; OpenAI `needsApproval` returns `true` and the run returns `interruptions`; both resolve through the `ApprovalStore` |
| Terminal | stderr text or `--json` Problem Details, exit `77` (`EX_NOPERM`) | Interactive TTY confirm; non-interactive exits `75` (`EX_TEMPFAIL`) with an `approval` extension |
| WebMCP | Tool not registered, or a tool error whose text carries the Decision reason and `alternatives` | Tool result asking the agent to obtain page confirmation; `onApprovalRequired` may resolve it |
| A2A | Skill absent from the extended card; task failure with Problem Details | Task `input-required` with the Decision token |

Denial bodies are written for models as well as humans: `denials[].reason` is a sentence, `alternatives` lists permitted permissions on the same resource. See [errors](/docs/concepts/errors) and [Problem Details](/docs/standards/problem-details).

Tenancy adds four reason codes every surface carries unchanged: `tenant-mismatch` (the row belongs to another tenant), `no-membership` (the active tenant is not one the subject belongs to), `scope` (the role is held, but not for this row or team) and `expired-membership`. HTTP bodies add `tenant` next to `permission`; MCP `structuredContent` and agent denials carry the same fields so a model can tell "wrong workspace" from "not allowed" ([tenancy](/docs/concepts/tenancy)).

Authentication adds two more that every surface renders from the reason alone: `on('auth')` failures are `reason: 'invalid-token'` with a `cause` (the token becomes anonymous, so the Decision reason a caller sees is `anonymous`), and a failed `subject.assurance` condition is `insufficient-user-authentication`, the RFC 9470 step-up; agent surfaces carry the required `acr` in the denial so the runtime can ask the user to re-authenticate rather than retry.

### UI parity [#ui-parity]

The UI entries expose the same surface under each framework's idiom. A hook that exists in `permdock/react` exists in every UI adapter ([UI concept](/docs/concepts/ui)).

| Surface | react, react-native | next (client half) | vue | svelte | solid | webmcp |
| --- | --- | --- | --- | --- | --- | --- |
| `PermDockProvider` / snapshot install | `PermDockProvider` | `PermDockProvider` from the factory | plugin install | context setter | `PermDockProvider` | `registerTools({ snapshot })` |
| `snapshotPromise` (hooks answer `pending` until it resolves) | react only; React Native boots from storage | yes (the server `PermDockProvider` streams one) | promise `snapshot`; await it in an async `setup` under `<Suspense>` | promise `snapshot`; render it with `{#await}` | promise `snapshot` or a `createResource` accessor | — |
| Reactive snapshot source (re-hydrates on change) | a new `snapshot` prop rebuilds the store | same | ref or getter | getter or readable store | accessor | reads the current store instance |
| `usePermDock` | yes | yes | composable | context getter | yes | internal |
| `usePermission`, `usePermissions` | yes | yes (`getPermission` on the server) | composables | `permission`, `permissions` stores | signal accessors | tool filter |
| `useFilter` | yes | yes | composable | derived store | signal accessor | — |
| `useTenant`, `useMemberships`, `useRoles`, `useAssignableRoles`, `useAssignablePermissions` | yes | yes (`getPermDock().tenants()` on the server) | composables | stores | signal accessors | `registerTools({ tenant })` |
| `useApproval` | yes | yes | composable | store | signal accessor | — |
| `useSubject` | yes | yes | composable | store | signal accessor | — |
| `Protected` (with `fallback`, `tenant`) | yes | yes | component | component | component | — |
| `Protected` `approval` slot (falls back to `fallback`) | yes | yes | `#approval` slot | `approval` snippet | `approval` prop | — |
| Provider `defaults`, `messages` | yes | nodes and message records only | plugin options | context setter options | provider props | — |
| Provider `onSnapshot`, `onClear`, `approvalInterval` | yes | — | plugin options | context setter options | provider props | — |
| `useDescribe` (`describe` with the provider's `messages`) | yes | yes | composable | `getDescribe()` | yes | — |
| `PermissionBoundary`, `usePermissionBoundary` | none: catch with an error boundary and `parsePermDockDigest` | `permdock/next/client` | component over `onErrorCaptured` | component over `<svelte:boundary>`; the snippet gets the state | component over `ErrorBoundary` | — |
| `describe(decision)` | core export, framework-free | same | same | same | same | same |
| `invalidate()` / `refresh()` | yes | yes (`updateTag` on the app's snapshot tag) | yes | yes | yes | re-register |

### Naming rules [#naming-rules]

* `createPermDock` is the factory in every server and agent adapter. UI adapters export hooks and components directly because the permission reference already carries the types.
* Client hooks are `use*`; async server counterparts are `get*` (`usePermission` / `getPermission`).
* The request-scoped instance is always reachable under the name `permdock` (`c.get('permdock')`, `req.permdock`, `ctx.permdock`).
* No framework name in identifiers, no `dock`, no `ability`, no `$`-prefixed members.

### Adapter contribution checklist [#adapter-contribution-checklist]

An adapter is complete when all five artefacts exist and cross-reference each other (`.agents/rules/change-checklist.mdc` carries the same list as "when you change X also update Y"):

1. Docs page under `apps/docs/content/docs/adapters/<name>.mdx` with the standard sections, a `Status: planned` line only while it is unbuilt, plus a row in the matrix above and an entry in `meta.json`.
2. Skill reference: a section in the `permdock-wire` skill reference (or the topic skill that owns the adapter) so an agent can wire the adapter without reading source.
3. Example app under `apps/examples/<name>` that runs in CI and exercises deny, approval-required and the framework's OpenAPI or guard integration where applicable.
4. Catalog: the adapter's public identifiers registered in the `permdock doctor` checks and, for HTTP adapters, the OpenAPI hook covered by `permdock openapi`.
5. Tests: unit tests next to the entry, a `tests/types` case for the typed generics, and a `tests/bundle` budget for the entry.
