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.

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 (permix adapters each copy request isolation; see 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

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, 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

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 and run on the UI / HTTP adapters.

AdapterImport pathWhat createPermDock (or the entry) returnsExample appRelated standards
reactpermdock/reactDirect exports, no factory: PermDockProvider, usePermDock, usePermission, Protectedapps/examples/react-vite, apps/examples/react-routerAuthZEN, Problem Details
react-nativepermdock/react-nativeSame exports as React plus a storage option for a persisted snapshotapps/examples/expoAuthZEN
nextpermdock/nextgetPermDock, getPermission, requireAccess, PermDockProvider, permdockHandler; PermissionBoundary in permdock/next/client; OpenAPI via next-openapi-gen plus the PermDock Overlayapps/examples/next, apps/examples/next-better-supabaseAuthZEN, Problem Details, OpenAPI Overlay
vuepermdock/vuePlugin install plus usePermDock / usePermission composables, Protected and PermissionBoundaryapps/examples/vueAuthZEN
sveltepermdock/svelteContext setter plus permission store, Protected and PermissionBoundaryapps/examples/svelteAuthZEN
solidpermdock/solidPermDockProvider, usePermDock, usePermission signal accessor, Protected, PermissionBoundaryapps/examples/solidAuthZEN
server-kernelpermdock/serverFetch kernel: permdock(request), protect, problem, openapi hook contract; createServerKernel for out-of-tree adapters—Problem Details, OpenAPI 3.2, Web Bot Auth
honopermdock/honopermdock middleware and protect route guardapps/examples/honoProblem Details, OpenAPI 3.2, Web Bot Auth
expresspermdock/expresspermdock middleware and protect middlewareapps/examples/expressProblem Details, OpenAPI 3.2
fastifypermdock/fastifypermdock plugin (decorates request.permdock) and protect preHandlerapps/examples/fastifyProblem Details, OpenAPI 3.2
elysiapermdock/elysiapermdock plugin (derives permdock into context) and protect hookapps/examples/elysiaProblem Details, OpenAPI 3.2
nestpermdock/nestPermDockModule, PermDockGuard, Protect, InjectPermDockapps/examples/nestProblem Details, OpenAPI 3.2
nodepermdock/nodepermdock, protect, send over IncomingMessage; shared toRequest / fromResponse—Problem Details
terminalpermdock/terminalpermdock, protect, filterCommands, format for your own CLI (commander, citty, oclif, yargs, Ink)apps/examples/terminalProblem Details, FAPI 2.0
trpcpermdock/trpcpermdock context helper and protect middleware; trpc-to-openapi protect hookapps/examples/trpcOpenAPI 3.2, Problem Details
orpcpermdock/orpcpermdock middleware and protect middleware; openapi() OpenAPI metadataapps/examples/orpcOpenAPI 3.2, Problem Details
mcppermdock/mcpprotectServer (hosted through mcp-handler, the SDK middleware or McpAgent)apps/examples/mcp-serverMCP authorization, OAuth agent delegation
ai-sdkpermdock/ai-sdktoolApproval, capabilityMiddleware, needsApprovalapps/examples/ai-sdk-agentOAuth agent delegation
claude-agentpermdock/claude-agentcanUseTool, permissionRequestHookapps/examples/claude-agentOAuth agent delegation
evepermdock/eveapproval (request and response policies for defineTool), approvalFor, permdockapps/examples/eve-agent (also the Marketplace template)OAuth agent delegation
openaipermdock/openaineedsApproval, guardTools, resolveInterruptions, permdockapps/examples/openai-agentMCP authorization (hosted MCP tools)
webmcppermdock/webmcpregisterTools (client entry, no factory)apps/examples/webmcpWebMCP
a2apermdock/a2aagentCard, extendedAgentCard, protectSkillapps/examples/a2a-agentA2A
authzenpermdock/authzenpermdockHandler serving evaluation, evaluations, search and discoveryapps/examples/authzen-pdpAuthZEN
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)Problem Details
cloudpermdock/cloudNo factory: cloud({ url, key }) returning approvals, sink, snapshots for PermDock Cloud (https://app.permdock.com, https://api.permdock.com); never on the decision pathapps/examples/eve-agentAuthZEN, Shared Signals and CAEP
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, Shared Signals and CAEP, SCIM, MCP authorization
ssfpermdock/ssfreceiver for CAEP Security Event Tokens—Shared Signals and CAEP
scimpermdock/scimNo factory: scimHandler (RFC 7644 /Users and /Groups receiver), DirectoryStore with memoryDirectoryStore, directoryMembershipSource (a MembershipSource over the synced groups); PermDock Cloud relays provisioning to itapps/examples/scimSCIM, JWT authorization claims
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 bridges—OpenAPI 3.2, OpenAPI Overlay
otelpermdock/otelinstrument / withOtel (on('decision') spans and counters)— (Hono and MCP examples enable otel)OpenTelemetry GenAI
drizzlepermdock/drizzletoWhere condition compilerapps/examples/drizzlePostgres RLS
prismapermdock/prismatoWhere condition compilerapps/examples/prismaPostgres RLS
kyselypermdock/kyselytoWhere condition compiler—Postgres RLS
rlspermdock rls (CLI)generate, import, verifyapps/examples/supabase-rlsPostgres 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)FAPI 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-rls, apps/examples/next-better-supabasePostgres RLS, JWT authorization claims
supabase (middleware)permdock/supabase/middlewarewithPermDock (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 recipesapps/examples/supabase-middlewareProblem Details, AuthZEN
better-supabasepermdock/better-supabaseNo factory: authorizationProvider (better-supabase's authorization config), bucketPolicy and topicPolicy, apiKeyVerifier, apiKeyClaimOptions, subjectFromBetterSupabaseapps/examples/next-better-supabase—
better-authpermdock/better-authProvider: subjectFromBetterAuth (session, organization and team memberships), betterAuthRoleSource (dynamic roles as a RoleSource), rolesFromAccessControlapps/examples/better-auth—
clerkpermdock/clerkProvider: subjectFromClerk (session, active organization as tenant, memberships; memberships: 'all' through the Backend API), custom roles and role sets as a RoleSourceapps/examples/clerk—
convexpermdock/convexProvider: Convex identity to subject inside functionsapps/examples/convex—
pdppermdock/pdpAuthZEN PEP client that answers checks from a remote PDP, plus openfga and spicedb presetsapps/examples/authzen-pdpAuthZEN
catalogpermdock/catalogNo factory: parseCatalog (validates and freezes a permissions.catalog.json), rowConditionKeys, the Catalog* typesapps/examples/next-better-supabase—
compilepermdock/compileNo 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
testingpermdock/testingPolicy matrix tests, snapshot fixtures, RLS parity runner, conformance runners—Postgres RLS

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.

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. A DirectoryStore 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

  • 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, 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)403 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; 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 setinput_required URL request when approval.at is set and the client takes URL elicitations; otherwise a refusal carrying token
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
Terminalstderr text or --json Problem Details, exit 77 (EX_NOPERM)Interactive TTY confirm; non-interactive exits 75 (EX_TEMPFAIL) with an approval extension
WebMCPTool not registered, or a tool error whose text carries the Decision reason and alternativesTool result asking the agent to obtain page confirmation; onApprovalRequired may resolve it
A2ASkill absent from the extended card; task failure with Problem DetailsTask 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 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).

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

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

Surfacereact, react-nativenext (client half)vuesveltesolidwebmcp
PermDockProvider / snapshot installPermDockProviderPermDockProvider from the factoryplugin installcontext setterPermDockProviderregisterTools({ snapshot })
snapshotPromise (hooks answer pending until it resolves)react only; React Native boots from storageyes (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 storesameref or gettergetter or readable storeaccessorreads the current store instance
usePermDockyesyescomposablecontext getteryesinternal
usePermission, usePermissionsyesyes (getPermission on the server)composablespermission, permissions storessignal accessorstool filter
useFilteryesyescomposablederived storesignal accessor—
useTenant, useMemberships, useRoles, useAssignableRoles, useAssignablePermissionsyesyes (getPermDock().tenants() on the server)composablesstoressignal accessorsregisterTools({ tenant })
useApprovalyesyescomposablestoresignal accessor—
useSubjectyesyescomposablestoresignal accessor—
Protected (with fallback, tenant)yesyescomponentcomponentcomponent—
Protected approval slot (falls back to fallback)yesyes#approval slotapproval snippetapproval prop—
Provider defaults, messagesyesnodes and message records onlyplugin optionscontext setter optionsprovider props—
Provider onSnapshot, onClear, approvalIntervalyes—plugin optionscontext setter optionsprovider props—
useDescribe (describe with the provider's messages)yesyescomposablegetDescribe()yes—
PermissionBoundary, usePermissionBoundarynone: catch with an error boundary and parsePermDockDigestpermdock/next/clientcomponent over onErrorCapturedcomponent over <svelte:boundary>; the snippet gets the statecomponent over ErrorBoundary—
describe(decision)core export, framework-freesamesamesamesamesame
invalidate() / refresh()yesyes (updateTag on the app's snapshot tag)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/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.

Last updated on

On this page