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,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 (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, andpublintandarethetypeswrongkeep theexportsmap honest. Scoped per-adapter packages were rejected for install friction and version skew. The test runners are a subpath too:permdock/testingis imported only from test files, and Vitest is an optional peer. The CLI ships in the same package as thepermdockbinary,permdock/cli,permdock/unpluginandpermdock/next/plugin. No runtime entry imports CLI code, so its parsers never land in an application bundle;oxc-parseris the one CLI dependency, and the Postgres (WASM) parser,pgandunpluginare optional peers loaded only by the commands that need them. - Wire-format readers are runtime entries.
permdock/catalogreadspermissions.catalog.json(parseCatalog, theCatalog*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 fromschemas/catalog-v1.json. The reader andpermdock catalog --format schemashare 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/solidandpermdock/webmcpimport 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 intests/bundleagainst 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.
| Adapter | Import path | What createPermDock (or the entry) returns | Example app | Related standards |
|---|---|---|---|---|
| react | permdock/react | Direct exports, no factory: PermDockProvider, usePermDock, usePermission, Protected | apps/examples/react-vite, apps/examples/react-router | AuthZEN, Problem Details |
| react-native | permdock/react-native | Same exports as React plus a storage option for a persisted snapshot | apps/examples/expo | AuthZEN |
| 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, Problem Details, OpenAPI Overlay |
| vue | permdock/vue | Plugin install plus usePermDock / usePermission composables, Protected and PermissionBoundary | apps/examples/vue | AuthZEN |
| svelte | permdock/svelte | Context setter plus permission store, Protected and PermissionBoundary | apps/examples/svelte | AuthZEN |
| solid | permdock/solid | PermDockProvider, usePermDock, usePermission signal accessor, Protected, PermissionBoundary | apps/examples/solid | AuthZEN |
| server-kernel | permdock/server | Fetch kernel: permdock(request), protect, problem, openapi hook contract; createServerKernel for out-of-tree adapters | — | Problem Details, OpenAPI 3.2, Web Bot Auth |
| hono | permdock/hono | permdock middleware and protect route guard | apps/examples/hono | Problem Details, OpenAPI 3.2, Web Bot Auth |
| express | permdock/express | permdock middleware and protect middleware | apps/examples/express | Problem Details, OpenAPI 3.2 |
| fastify | permdock/fastify | permdock plugin (decorates request.permdock) and protect preHandler | apps/examples/fastify | Problem Details, OpenAPI 3.2 |
| elysia | permdock/elysia | permdock plugin (derives permdock into context) and protect hook | apps/examples/elysia | Problem Details, OpenAPI 3.2 |
| nest | permdock/nest | PermDockModule, PermDockGuard, Protect, InjectPermDock | apps/examples/nest | Problem Details, OpenAPI 3.2 |
| node | permdock/node | permdock, protect, send over IncomingMessage; shared toRequest / fromResponse | — | Problem Details |
| terminal | permdock/terminal | permdock, protect, filterCommands, format for your own CLI (commander, citty, oclif, yargs, Ink) | apps/examples/terminal | Problem Details, FAPI 2.0 |
| trpc | permdock/trpc | permdock context helper and protect middleware; trpc-to-openapi protect hook | apps/examples/trpc | OpenAPI 3.2, Problem Details |
| orpc | permdock/orpc | permdock middleware and protect middleware; openapi() OpenAPI metadata | apps/examples/orpc | OpenAPI 3.2, Problem Details |
| mcp | permdock/mcp | protectServer (hosted through mcp-handler, the SDK middleware or McpAgent) | apps/examples/mcp-server | MCP authorization, OAuth agent delegation |
| ai-sdk | permdock/ai-sdk | toolApproval, capabilityMiddleware, needsApproval | apps/examples/ai-sdk-agent | OAuth agent delegation |
| claude-agent | permdock/claude-agent | canUseTool, permissionRequestHook | apps/examples/claude-agent | OAuth agent delegation |
| eve | permdock/eve | approval (request and response policies for defineTool), approvalFor, permdock | apps/examples/eve-agent (also the Marketplace template) | OAuth agent delegation |
| openai | permdock/openai | needsApproval, guardTools, resolveInterruptions, permdock | apps/examples/openai-agent | MCP authorization (hosted MCP tools) |
| webmcp | permdock/webmcp | registerTools (client entry, no factory) | apps/examples/webmcp | WebMCP |
| a2a | permdock/a2a | agentCard, extendedAgentCard, protectSkill | apps/examples/a2a-agent | A2A |
| authzen | permdock/authzen | permdockHandler serving evaluation, evaluations, search and discovery | apps/examples/authzen-pdp | 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) | Problem Details |
| 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, 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 |
| ssf | permdock/ssf | receiver for CAEP Security Event Tokens | — | Shared Signals and CAEP |
| 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, JWT authorization claims |
| 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, OpenAPI Overlay |
| otel | permdock/otel | instrument / withOtel (on('decision') spans and counters) | — (Hono and MCP examples enable otel) | OpenTelemetry GenAI |
| drizzle | permdock/drizzle | toWhere condition compiler | apps/examples/drizzle | Postgres RLS |
| prisma | permdock/prisma | toWhere condition compiler | apps/examples/prisma | Postgres RLS |
| kysely | permdock/kysely | toWhere condition compiler | — | Postgres RLS |
| rls | permdock rls (CLI) | generate, import, verify | apps/examples/supabase-rls | 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) | 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, apps/examples/next-better-supabase | Postgres RLS, JWT authorization claims |
| supabase (middleware) | 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, AuthZEN |
| 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 | permdock/better-auth | Provider: subjectFromBetterAuth (session, organization and team memberships), betterAuthRoleSource (dynamic roles as a RoleSource), rolesFromAccessControl | apps/examples/better-auth | — |
| 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 | permdock/convex | Provider: Convex identity to subject inside functions | apps/examples/convex | — |
| pdp | permdock/pdp | AuthZEN PEP client that answers checks from a remote PDP, plus openfga and spicedb presets | apps/examples/authzen-pdp | AuthZEN |
| catalog | permdock/catalog | No factory: parseCatalog (validates and freezes a permissions.catalog.json), rowConditionKeys, the Catalog* types | apps/examples/next-better-supabase | — |
| compile | 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 |
| testing | permdock/testing | Policy 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
- 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. ADirectoryStoreis the write side of the same input:scimHandlerfills it from an authenticated identity provider (or the Cloud's relay) anddirectoryMembershipSourcereads it as aMembershipSource; 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
.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. 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 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 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).
| 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
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/rules/change-checklist.mdc carries the same list as "when you change X also update Y"):
- Docs page under
apps/docs/content/docs/adapters/<name>.mdxwith the standard sections, aStatus: plannedline only while it is unbuilt, plus a row in the matrix above and an entry inmeta.json. - Skill reference: a section in the
permdock-wireskill reference (or the topic skill that owns the adapter) 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, and atests/bundlebudget for the entry.
Last updated on
Support access
Let staff act inside a customer's account with better-supabase support sessions and a PermDock delegation, read-only by default, enforced in process and in Postgres, and attributed on every decision.
React
permdock/react gives client components a snapshot-backed PermDock through PermDockProvider, usePermDock, usePermission, usePermissions, useFilter, useTenant, useMemberships, useRoles, useAssignableRoles, useAssignablePermissions, useApproval, useSubject and Protected, with a batched decision endpoint for closure grants.