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.
Status: planned Phase: 1
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
- One evaluation path.
can,decide,assert,filter,where,simulateandsnapshotlive 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. - A Fetch-first kernel.
permdock/serverspeaksRequest/Response. Hono, Express, Fastify, Elysia, Nest and Node adapters wrap the kernel with a few lines of typed glue instead of five separate implementations (the pattern permix reached only in a rejected PR; see permix lessons). - Client entries are server-free.
permdock/react,permdock/react-native,permdock/vue,permdock/svelte,permdock/solidandpermdock/webmcpimport only the definition and a snapshot. Policies and subject resolvers never enter a client bundle. - Size stays small by construction: ESM-only, zero runtime dependencies in core, one types-only spec package. Each entry's gzip size is measured in
tests/bundle; a regression baseline is recorded after core ships, not guessed in advance (0020).
Adapter matrix
Phases come from the roadmap. Everything is planned in Phase 0. Example apps live under apps/examples/<name>.
| Adapter | Import path | What createPermDock (or the entry) returns | Example app | Phase | Related standards |
|---|---|---|---|---|---|
| react | permdock/react | Direct exports, no factory: PermDockProvider, usePermDock, usePermission, Protected | apps/examples/react-vite | 1 | AuthZEN, Problem Details |
| react-native | permdock/react-native | Same exports as React plus a storage option for a persisted snapshot | apps/examples/expo | 2 | AuthZEN |
| next | permdock/next | getPermDock, getPermission, PermDockProvider, permdockHandler; OpenAPI via next-openapi-gen plus the PermDock Overlay | apps/examples/next | 1 | AuthZEN, Problem Details, OpenAPI Overlay |
| vue | permdock/vue | Plugin install plus usePermDock / usePermission composables and a Protected component (names open) | apps/examples/vue | 2 | AuthZEN |
| svelte | permdock/svelte | Context setter plus permission store and a Protected component (names open) | apps/examples/svelte | 2 | AuthZEN |
| solid | permdock/solid | PermDockProvider, usePermDock, usePermission signal accessor, Protected (names open) | apps/examples/solid | 2 | AuthZEN |
| server-kernel | permdock/server | Fetch kernel: permdock(request), protect, problem, openapi hook contract | — | 1 | Problem Details, OpenAPI 3.2, Web Bot Auth |
| hono | permdock/hono | permdock middleware and protect route guard | apps/examples/hono | 1 | Problem Details, OpenAPI 3.2, Web Bot Auth |
| express | permdock/express | permdock middleware and protect middleware | apps/examples/express | 2 | Problem Details, OpenAPI 3.2 |
| fastify | permdock/fastify | permdock plugin (decorates request.permdock) and protect preHandler | apps/examples/fastify | 2 | Problem Details, OpenAPI 3.2 |
| elysia | permdock/elysia | permdock plugin (derives permdock into context) and protect hook | apps/examples/elysia | 2 | Problem Details, OpenAPI 3.2 |
| nest | permdock/nest | PermDockModule, PermDockGuard, Protect decorator (names open) | apps/examples/nest | 2 | Problem Details, OpenAPI 3.2 |
| node | permdock/node | permdock and protect over http.IncomingMessage / http.ServerResponse | — | 2 | Problem Details |
| terminal | permdock/terminal | permdock, protect, filterCommands, format for your own CLI (commander, citty, oclif, yargs, Ink) | apps/examples/terminal | 2 | Problem Details, FAPI 2.0 |
| trpc | permdock/trpc | permdock context helper and protect middleware; trpc-to-openapi protect hook | apps/examples/trpc | 2 | OpenAPI 3.2, Problem Details |
| orpc | permdock/orpc | permdock middleware and protect middleware; oo.spec OpenAPI hook | apps/examples/orpc | 2 | OpenAPI 3.2, Problem Details |
| mcp | permdock/mcp | protectServer (hosted through mcp-handler, the SDK middleware or McpAgent) | apps/examples/mcp-server | 2 | MCP authorization, OAuth agent delegation |
| ai-sdk | permdock/ai-sdk | toolApproval, capabilityMiddleware, needsApproval | apps/examples/ai-sdk-agent | 1 | OAuth agent delegation |
| claude-agent | permdock/claude-agent | canUseTool, permissionRequestHook | apps/examples/claude-agent | 1 | OAuth agent delegation |
| eve | permdock/eve | approval (request and response policies for defineTool), approvalFor, permdock | apps/examples/eve-agent (also the Marketplace template) | 1 | OAuth agent delegation |
| openai | permdock/openai | needsApproval, guardTools, resolveInterruptions, permdock | apps/examples/openai-agent | 1 | MCP authorization (hosted MCP tools) |
| webmcp | permdock/webmcp | registerTools (client entry, no factory) | apps/examples/webmcp | 2 | WebMCP |
| a2a | permdock/a2a | agentCard, extendedAgentCard | apps/examples/a2a-agent | 2 | A2A |
| authzen | permdock/authzen | handler serving evaluation, evaluations, search and discovery | apps/examples/authzen-pdp | 2 | AuthZEN |
| 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) | 1 | Problem Details |
| cloud | permdock/cloud | No factory: cloud({ url, key }) returning approvals, sink, snapshots for PermDock Cloud; never on the decision path | apps/examples/eve-agent | 2 | AuthZEN, Shared Signals and CAEP |
| ssf | permdock/ssf | receiver for CAEP Security Event Tokens | — | 3 | Shared Signals and CAEP |
| 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 | — | 2 | OpenAPI 3.2, OpenAPI Overlay |
| otel | permdock/otel | on('decision') listener that emits one span per check | — | 2 | — |
| drizzle | permdock/drizzle | toWhere condition compiler | apps/examples/drizzle | 3 | Postgres RLS |
| prisma | permdock/prisma | toWhere condition compiler | apps/examples/prisma | 3 | Postgres RLS |
| kysely | permdock/kysely | toWhere condition compiler | — | 3 | Postgres RLS |
| rls | @permdock/cli (permdock rls) | generate, import, verify | apps/examples/supabase-rls | 3 | Postgres RLS |
| 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) | 1 | FAPI 2.0, OAuth agent delegation, JWT authorization claims |
| 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 | 3 | Postgres RLS, JWT authorization claims |
| better-auth | permdock/better-auth | Provider: subjectFromBetterAuth (session, organization and team memberships), betterAuthRoleSource (dynamic roles as a RoleSource), rolesFromAccessControl | apps/examples/better-auth | 4 | — |
| 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 | 4 | — |
| convex | permdock/convex | Provider: Convex identity to subject inside functions | apps/examples/convex | 4 | — |
| pdp | permdock/pdp | AuthZEN PEP client that answers checks from a remote PDP | apps/examples/authzen-pdp | 4 | AuthZEN |
| testing | @permdock/testing | Policy matrix tests, snapshot fixtures, RLS parity runner, instant() helpers | — | 1 | Postgres RLS |
The shared adapter contract
Every adapter page documents the same eight sections (Purpose, API, Request lifecycle, What it validates, How denials surface, Example app, Related standards, Open questions). The behaviour below is common to all of them.
Request lifecycle
- Subject resolve. The adapter calls the
subjectoption you passed tocreatePermDockwith the framework's native handle (cin Hono,reqin Express,authInfoin MCP,cookies()in Next.js). It may returnnullfor anonymous callers. Agent adapters also fillactoranddelegationfrom 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 bypermdock/jwtor a providersubjectFrom*helper. PermDock never authenticates; see authentication. The same rule coversprincipal.tenantandprincipal.memberships: the adapter'stenantoption 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). createPermDock. Exactly one immutablePermDockis 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 globalsetup().- Check. Handlers call
can,decide,assert,filterorwhereon the request-scoped instance, or letprotect(permission, loadData)do it before the handler runs. - Denial surface. The adapter turns a
deniedorapproval-requiredDecisioninto the framework's response type and forwards theon('decision')event to audit. See decisions. - Pluggable state. Every adapter that can surface
approval-requiredaccepts astore(anApprovalStore, in-memory by default) and every adapter accepts asink(aDecisionSink, in-memory by default). Neither is consulted on thedecidepath;permdock/cloudis one implementation of both, and an application database is another. Every adapter also acceptsmemberships(aMembershipSource) andcustomRoles(aRoleSource); 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.
What every adapter validates
- Permission references are typed; adapters never accept a permission string from the outside. Strings only appear as
.keyand.scopeon the wire. - Data that crossed a trust boundary (HTTP body, route params fed into
loadData, MCP tool arguments, clientrefreshpayloads on the decision endpoint) is validated against the resource's Standard Schema when the policy usesvalidate: 'boundary'(the default). Trusted server rows are not re-validated. See validation. - Snapshots and decision-endpoint requests are validated against the published wire schemas (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
| Surface | denied | approval-required |
|---|---|---|
| HTTP and RPC | 403 with application/problem+json: type, title, status, permission, denials, alternatives | 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; missing scope becomes a 403 insufficient_scope challenge | Elicitation request to the client |
| 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 |
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 and 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).
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 before the adapter's Status becomes shipped (UI concept).
| 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 }) |
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 | 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 | — |
describe(decision) | core export, framework-free | same | same | same | same | same |
invalidate() / refresh() | yes | yes (updateTag on the server) | yes | yes | yes | re-register |
Naming rules
createPermDockis 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 areget*(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, noability, no$-prefixed members.
Adapter contribution checklist
An adapter is complete when all five artefacts exist and cross-reference each other (AGENTS.md carries the same list as "when you change X also update Y"):
- Docs page under
apps/docs/content/docs/adapters/<name>.mdxwith the eight standard sections and truthfulStatus/Phaselines, plus a row in the matrix above and an entry inmeta.json. - Skill reference: a short file under the shipped
wire-permdockskill so an agent can wire the adapter without reading source. - 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. - Catalog: the adapter's public identifiers registered in the
permdock doctorchecks and, for HTTP adapters, the OpenAPI hook covered bypermdock openapi. - Tests: unit tests next to the entry, a
tests/typescase for the typed generics, atests/bundlebudget for the entry, and, where an example exists, an e2e run undertests/e2e.
Wire formats
The JSON shapes PermDock reads and writes, permission leaves, conditions, snapshot v2, memberships and custom roles, AuthZEN messages, the catalog, Decisions and Problem Details, with an example of each.
React
permdock/react gives client components a snapshot-backed PermDock through PermDockProvider, usePermDock, usePermission, usePermissions, useFilter, useTenant, useMemberships, useRoles, useAssignableRoles, useApproval, useSubject and Protected, with a batched decision endpoint for closure grants.