PermDock
Adapters

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, 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.
  • 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 (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/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, 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>.

AdapterImport pathWhat createPermDock (or the entry) returnsExample appPhaseRelated standards
reactpermdock/reactDirect exports, no factory: PermDockProvider, usePermDock, usePermission, Protectedapps/examples/react-vite1AuthZEN, Problem Details
react-nativepermdock/react-nativeSame exports as React plus a storage option for a persisted snapshotapps/examples/expo2AuthZEN
nextpermdock/nextgetPermDock, getPermission, PermDockProvider, permdockHandler; OpenAPI via next-openapi-gen plus the PermDock Overlayapps/examples/next1AuthZEN, Problem Details, OpenAPI Overlay
vuepermdock/vuePlugin install plus usePermDock / usePermission composables and a Protected component (names open)apps/examples/vue2AuthZEN
sveltepermdock/svelteContext setter plus permission store and a Protected component (names open)apps/examples/svelte2AuthZEN
solidpermdock/solidPermDockProvider, usePermDock, usePermission signal accessor, Protected (names open)apps/examples/solid2AuthZEN
server-kernelpermdock/serverFetch kernel: permdock(request), protect, problem, openapi hook contract1Problem Details, OpenAPI 3.2, Web Bot Auth
honopermdock/honopermdock middleware and protect route guardapps/examples/hono1Problem Details, OpenAPI 3.2, Web Bot Auth
expresspermdock/expresspermdock middleware and protect middlewareapps/examples/express2Problem Details, OpenAPI 3.2
fastifypermdock/fastifypermdock plugin (decorates request.permdock) and protect preHandlerapps/examples/fastify2Problem Details, OpenAPI 3.2
elysiapermdock/elysiapermdock plugin (derives permdock into context) and protect hookapps/examples/elysia2Problem Details, OpenAPI 3.2
nestpermdock/nestPermDockModule, PermDockGuard, Protect decorator (names open)apps/examples/nest2Problem Details, OpenAPI 3.2
nodepermdock/nodepermdock and protect over http.IncomingMessage / http.ServerResponse2Problem Details
terminalpermdock/terminalpermdock, protect, filterCommands, format for your own CLI (commander, citty, oclif, yargs, Ink)apps/examples/terminal2Problem Details, FAPI 2.0
trpcpermdock/trpcpermdock context helper and protect middleware; trpc-to-openapi protect hookapps/examples/trpc2OpenAPI 3.2, Problem Details
orpcpermdock/orpcpermdock middleware and protect middleware; oo.spec OpenAPI hookapps/examples/orpc2OpenAPI 3.2, Problem Details
mcppermdock/mcpprotectServer (hosted through mcp-handler, the SDK middleware or McpAgent)apps/examples/mcp-server2MCP authorization, OAuth agent delegation
ai-sdkpermdock/ai-sdktoolApproval, capabilityMiddleware, needsApprovalapps/examples/ai-sdk-agent1OAuth agent delegation
claude-agentpermdock/claude-agentcanUseTool, permissionRequestHookapps/examples/claude-agent1OAuth agent delegation
evepermdock/eveapproval (request and response policies for defineTool), approvalFor, permdockapps/examples/eve-agent (also the Marketplace template)1OAuth agent delegation
openaipermdock/openaineedsApproval, guardTools, resolveInterruptions, permdockapps/examples/openai-agent1MCP authorization (hosted MCP tools)
webmcppermdock/webmcpregisterTools (client entry, no factory)apps/examples/webmcp2WebMCP
a2apermdock/a2aagentCard, extendedAgentCardapps/examples/a2a-agent2A2A
authzenpermdock/authzenhandler serving evaluation, evaluations, search and discoveryapps/examples/authzen-pdp2AuthZEN
approvalspermdock/approvalsNo factory: ApprovalStore interface, memoryApprovalStore, approvalsHandler; the store option of every agent and HTTP adapter— (used by ai-sdk-agent, eve-agent, openai-agent, terminal)1Problem Details
cloudpermdock/cloudNo factory: cloud({ url, key }) returning approvals, sink, snapshots for PermDock Cloud; never on the decision pathapps/examples/eve-agent2AuthZEN, Shared Signals and CAEP
ssfpermdock/ssfreceiver for CAEP Security Event Tokens3Shared Signals and CAEP
openapipermdock/openapiSecurity 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 bridges2OpenAPI 3.2, OpenAPI Overlay
otelpermdock/otelon('decision') listener that emits one span per check2
drizzlepermdock/drizzletoWhere condition compilerapps/examples/drizzle3Postgres RLS
prismapermdock/prismatoWhere condition compilerapps/examples/prisma3Postgres RLS
kyselypermdock/kyselytoWhere condition compiler3Postgres RLS
rls@permdock/cli (permdock rls)generate, import, verifyapps/examples/supabase-rls3Postgres RLS
jwtpermdock/jwtsubjectFromJwt, createJwtSubjectResolver (JWKS, RFC 8725 checks, profile: 'fapi2', RFC 9068 roles / groups / entitlements to roles and memberships); jose optional peer— (used by hono, mcp-server)1FAPI 2.0, OAuth agent delegation, JWT authorization claims
supabasepermdock/supabaseProvider: subjectFromSupabase (roles, tenant and memberships from hook-injected claims), custom access token hook, authorize() RBAC scaffold, memberOf compilation to membership tablesapps/examples/supabase-rls3Postgres RLS, JWT authorization claims
better-authpermdock/better-authProvider: subjectFromBetterAuth (session, organization and team memberships), betterAuthRoleSource (dynamic roles as a RoleSource), rolesFromAccessControlapps/examples/better-auth4
clerkpermdock/clerkProvider: subjectFromClerk (session, active organization as tenant, memberships; memberships: 'all' through the Backend API), custom roles and role sets as a RoleSourceapps/examples/clerk4
convexpermdock/convexProvider: Convex identity to subject inside functionsapps/examples/convex4
pdppermdock/pdpAuthZEN PEP client that answers checks from a remote PDPapps/examples/authzen-pdp4AuthZEN
testing@permdock/testingPolicy matrix tests, snapshot fixtures, RLS parity runner, instant() helpers1Postgres 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

  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. 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).
  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.
  5. Pluggable state. Every adapter that can surface approval-required accepts a store (an ApprovalStore, in-memory by default) and every adapter accepts a sink (a DecisionSink, in-memory by default). Neither is consulted on the decide path; permdock/cloud is one implementation of both, and an application database is another. Every adapter also accepts memberships (a MembershipSource) 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.

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.
  • 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

Surfacedeniedapproval-required
HTTP and RPC403 with application/problem+json: type, title, status, permission, denials, alternatives403 with type ending in /approval-required, plus token
Next.js serverassert runs layered handlers; the default handler calls redirect() or notFound() as configuredSame handler chain; the app decides where to send the user
React and other UIusePermission returns allowed: false with status; Protected renders fallback; a disabled control uses usePermission plus describe(decision) for the reasonallowed: false, decision.outcome === 'approval-required' exposed for a "request access" UI; useApproval tracks the request
MCPTool result with isError: true and Decision reasons in structuredContent; missing scope becomes a 403 insufficient_scope challengeElicitation request to the client
AI SDK and Claude Agent SDKdenied with reason and alternativesuser-approval or needsApproval; the Claude hook returns an ask verdict
Eve and OpenAI Agents SDKEve { 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).

Surfacereact, react-nativenext (client half)vuesveltesolidwebmcp
PermDockProvider / snapshot installPermDockProviderPermDockProvider from the factoryplugin installcontext setterPermDockProviderregisterTools({ snapshot })
usePermDockyesyescomposablecontext getteryesinternal
usePermission, usePermissionsyesyes (getPermission on the server)composablespermission, permissions storessignal accessorstool filter
useFilteryesyescomposablederived storesignal accessor
useTenant, useMemberships, useRoles, useAssignableRolesyesyes (getPermDock().tenants() on the server)composablesstoressignal accessorsregisterTools({ tenant })
useApprovalyesyescomposablestoresignal accessor
useSubjectyesyescomposablestoresignal accessor
Protected (with fallback, tenant)yesyescomponentcomponentcomponent
describe(decision)core export, framework-freesamesamesamesamesame
invalidate() / refresh()yesyes (updateTag on the server)yesyesyesre-register

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

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"):

  1. Docs page under apps/docs/content/docs/adapters/<name>.mdx with the eight standard sections and truthful Status / Phase lines, plus a row in the matrix above and an entry in meta.json.
  2. Skill reference: a short file under the shipped wire-permdock skill 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, a tests/bundle budget for the entry, and, where an example exists, an e2e run under tests/e2e.

On this page