PermDock
Getting started

Naming

The PermDock naming convention, the use* and get* duality, reserved words, and what each adapter's createPermDock returns.

PermDock has one naming rule: the brand is the noun. The decision object is a PermDock, the variable is permdock, the factory is createPermDock, and the import path, not the identifier, says which framework you are in. Every package, docs page, skill, error message and example follows this page. The rationale is recorded in ADR 0005.

The three core names

NameKindMeaning
PermDocktypeThe immutable, request-scoped decision object returned by createPermDock. Methods: can, decide, assert, filter, where, simulate, snapshot, on; tenancy: tenant, team (derived instances), memberships, tenants, roles, assignable (read-only).
permdockvariableThe conventional name for a PermDock instance, and the npm package name. Also the CLI binary (permdock collect).
createPermDockfunctionExported by core and by every server and agent adapter. Core returns a PermDock; adapters return a framework-shaped object.
import { createPermDock } from 'permdock'        // core: PermDock
import { createPermDock } from 'permdock/next'   // Next.js: { getPermDock, getPermission, PermDockProvider, permdockHandler }
import { createPermDock } from 'permdock/hono'   // Hono: { permdock, protect }
import { createPermDock } from 'permdock/mcp'    // MCP: { protectServer }

There is no NextDock, HonoDock, createNextPermDock or createMcpPermDock. When two adapters are used in one file, alias at the import: import { createPermDock as createHonoPermDock } from 'permdock/hono'.

use* on the client, get* on the server

React reads permissions synchronously from a snapshot; servers resolve them asynchronously per request. PermDock follows the duality next-intl uses for useTranslations / getTranslations and useExtracted / getExtracted:

Client (permdock/react)Server (createPermDock from permdock/next)Returns
usePermDock()await getPermDock()A PermDock (snapshot-backed on the client, full policy on the server)
usePermission(permission, data?)await getPermission(permission, data?){ allowed, status } on the client; the same shape awaited on the server
usePermissions([...], data?), useFilter(permission, rows)(await getPermDock()).decide per reference, .filterOne entry per reference; the rows the subject may act on
useTenant(), useMemberships(), useRoles(), useAssignableRoles()(await getPermDock()).tenants(), .memberships(), .roles(), .assignable()The active tenant with switchTo; the membership list; roles held in a tenant; roles the subject may hand out
useApproval(decision), useSubject()the ApprovalStore and the subject on the serverClient-side request-access flow; the snapshot's principal, actor and delegation
<PermDockProvider snapshot endpoint tenant><PermDockProvider> from the factory resultClient context; the server variant loads the snapshot once per request and serialises it

The client hooks and <Protected> are direct exports with no factory: the permission reference carries all the types, so usePermission(permissions.post.update, post) is fully typed without a generic wrapper. Only the server side needs a factory, because only the server holds the policy. describe(decision) is a framework-free core export used by every UI adapter and by Problem Details (UI). Vue, Svelte and Solid expose the same names in their own idiom (composables, stores, signal accessors); the UI parity table lists them.

Definition and policy vocabulary

WordUsed forNever used for
definePermissionsBuild the reference tree from resources and groupsRules
resourceOne resource node: schema, id field, actions, collection, optional parent (the field and resource a resource role derives through)Anything without a schema-or-actions shape
actionsInstance-level actions (can(permissions.post.update, post))Type-level checks
collectionType-level actions (can(permissions.post.create))Instance checks
mergePermissions, listPermissions, findPermissionRegistry helpers; functions so they never collide with resource namesMethods on the tree
definePolicyBind roles, scopes, subject, context, validate to a definitionDefining permissions
roleA named array of grants with an optional scope ({ on: 'tenant' | 'team' | resource reference, assignable }); same-named roles mergePostgres roles
scopesThe definePolicy option naming the tenant and team key fields on rows ({ tenant: { key: 'orgId' }, team: { key: 'teamId' } })Permission scopes (.scope is the colon string form)
tenant, team, onThe three shapes of a Membership (tenant role, team role, resource role) and the on option of roleorg, organization, workspace, group as identifiers; those are provider vocabulary the mapper translates
Membership, CustomRoleThe wire types for a scoped role assignment and a tenant-defined role composed of declared assignable rolesClasses; both are plain JSON
MembershipSource, RoleSourceThe two subject-input interfaces (membershipsFor, rolesFor / assignable) passed as memberships and customRoles to every createPermDockStores; PermDock never writes a membership or role
SubjectResolverThe generic type of every subjectFrom* function: verified input in, Subject out, never throwsA class hierarchy
memoryRoleSourceThe in-process RoleSource over a static CustomRole[]Production storage
allow, denyThe two grant constructors; both accept one reference or an arrayChecking
subjectBoth the reference object used in conditions (subject.id) and the definePolicy option that maps a user to principal valuesThe agent (that is actor)
principal, actor, delegationThe three parts of a subjectSynonyms for user or role
granted, denied, approval-requiredThe three Decision.outcome valuesnot-applicable, allow, deny
where, checkConditions on the current row and on the next rowQuery building outside conditions
key, scopeThe dotted (post.update) and colon (post:update) string forms of a permissionPublic API arguments
subjectFrom<Provider>Turning verified material into a Subject with roles, tenant and memberships: subjectFromJwt, subjectFromSupabase, subjectFromClerk, subjectFromBetterAuth, subjectFromMcp; every one accepts a schema (Standard Schema) for custom claimsVerifying, logging in, or anything that can throw
createJwtSubjectResolverThe cached, reusable form of subjectFromJwt (one per issuer, JWKS cache); options claims.roles / groups / entitlements / tenant / memberships and groupRolesA second createPermDock
<provider>RoleSourceA provider's RoleSource implementation (betterAuthRoleSource)A subject mapper
describedescribe(decision) returning { kind, title, detail, alternatives } for tooltips and Problem DetailsLogging; it is pure
ApprovalStore, memoryApprovalStore, approvalsHandlerThe pluggable store behind approval-required, its in-process default, and the Fetch routes for approvers (permdock/approvals)Deciding; a store never influences decide
DecisionSink, memorySinkThe pluggable destination for on('decision') events and its in-process defaultBlocking a decision; sinks are fire-and-forget
store, sink, snapshotsThe three options every adapter's createPermDock accepts for an ApprovalStore, a DecisionSink and a SnapshotSourceAnything else; store is never a database handle
memberships, customRoles, tenantThe three tenancy options every adapter's createPermDock accepts: a MembershipSource, a RoleSource, and how the active tenant is resolved from the requestReading a tenant from an unsigned header or a model argument
cloudcloud({ url, key }) from permdock/cloud, returning approvals, sink and snapshots for PermDock CloudA factory for a PermDock; there is no decide on it
PermDock-ApprovalThe HTTP request header carrying an approval token on a retried requestAnything else on the wire

Strings appear only as .key and .scope on the wire, in audit events and in catalogs. The public API takes references.

The subjectFrom<Provider> pattern names the source of the material (a JWT, a Supabase claims object, a Clerk auth object, a Better Auth session, MCP authInfo), always returns a Subject (anonymous when the material cannot be trusted) and never throws, so every provider reads the same way at the call site: createPermDock(policy, subjectFromClerk(await auth())). See Authentication and PermDock.

Reserved words

These never appear as public identifiers, for the reasons given:

WordWhy it is reserved
dockEarly sketches used it as the instance name; it reads oddly next to PermDock and collides with nothing meaningful. Only permdock is used.
abilityCASL's noun; using it would imply CASL semantics (subject detection, manage/all) that PermDock does not have.
can as a definerCASL uses can both to define and to check rules, which its own cookbook documents as confusing (less-confusing-can-api). PermDock defines with allow / deny and checks with can.
$-prefixed membersKilpi prefixes every instance member with $ to avoid clashing with policy names in a Proxy tree. PermDock keeps methods on the instance and the tree separate, so no prefix is needed.
explainReplaced by decide, which returns the full Decision rather than a message (ADR 0007).
Can, Guard, AccessComponent names already used by CASL, older sketches and Kilpi; the PermDock component is <Protected>.
org, organization, workspace, account, group as PermDock identifiersEvery provider picks a different noun for the same thing. PermDock uses tenant and team in its own API and lets the subjectFrom* mapper translate (Clerk organization to tenant, SCIM group to team).
useOrganization, useTeam, TenantProviderThe tenant is a property of the one snapshot, not a second provider; useTenant and useMemberships read it.
hasRole, isAdmin, roleGuardRole checks in application code bypass the permission model; check a permission, and read roles only for display (useRoles) or assignment (useAssignableRoles).

What each adapter's createPermDock returns

Import pathReturnsNotes
permdockPermDockawait createPermDock(policy, user, { tenant?, memberships?, customRoles?, actor?, delegation? }); sync when the policy declares no context and no async source
permdock/next{ getPermDock, getPermission, PermDockProvider, permdockHandler }Options: subject, tag, tenant, memberships, customRoles, store, sink, snapshots
permdock/hono{ permdock, protect }permdock() is middleware; protect(permission, load?) guards a route
permdock/express, permdock/fastify, permdock/elysia, permdock/nest, permdock/node{ permdock, protect }Same shape as Hono over the shared kernel
permdock/terminal{ permdock, protect, filterCommands, format }For your own CLI; protect wraps a command action, filterCommands hides or annotates denied commands. Not @permdock/cli
permdock/trpc, permdock/orpc{ permdock, protect }protect is a procedure middleware
permdock/mcp{ protectServer }protectServer(server).registerTool(name, { permission, ... }, handler)
permdock/ai-sdk{ toolApproval, capabilityMiddleware, needsApproval }Options: subject, actor, tools
permdock/claude-agent{ canUseTool, permissionRequestHook }Options: subject, tools
permdock/eve{ approval, approvalFor, permdock }approval is Eve's { request, response } pair for defineTool; options: tools, store, approvers
permdock/openai{ needsApproval, guardTools, resolveInterruptions, permdock }Options: subject, actor, tools, store
permdock/authzen{ handler }Serves evaluation, evaluations, search and .well-known
permdock/ssf{ receiver }RFC 8935 push and RFC 8936 poll receiver for CAEP events
permdock/a2a{ agentCard, extendedAgentCard }Options: skills

Two entries deliberately do not export createPermDock:

  • permdock/react (and react-native, vue, svelte, solid) export PermDockProvider, usePermDock, usePermission, usePermissions, useFilter, useTenant, useMemberships, useRoles, useAssignableRoles, useApproval, useSubject and <Protected> directly.
  • permdock/better-auth, permdock/clerk and permdock/supabase export their subjectFrom* mapper and, where the provider stores roles, a <provider>RoleSource; both are values you pass to a factory.
  • permdock/webmcp exports registerTools(document.modelContext, group, { permdock }) because it consumes a client PermDock rather than creating one. permdock/drizzle, permdock/prisma and permdock/kysely export toWhere. permdock/jwt exports subjectFromJwt and createJwtSubjectResolver because it produces a subject for a factory, not an instance.
  • permdock/approvals exports memoryApprovalStore and approvalsHandler, and permdock/cloud exports cloud; both produce values you pass to a factory (store, sink, snapshots) rather than a PermDock.
  • Build hooks live in @permdock/cli, not in permdock: permdock/next/plugin exports createPermDockPlugin and @permdock/cli/unplugin exports createPermDockUnplugin (Vite, Rollup, webpack, Rspack, esbuild through unplugin). Both run collect at build time and never wire runtime API (0006). The create prefix and the PermDock noun follow the factory rule; the suffix names the host (Plugin for Next's config API, Unplugin for the bundler family).

Files and folders

  • The server factory lives in src/permdock/server.ts in every example. The definition is src/permissions.ts; the policy is src/policy.ts; generated definitions are src/permissions.generated.ts.
  • Example apps are apps/examples/<adapter>; the docs page is /docs/adapters/<adapter>; the source folder is packages/permdock/src/<adapter>. The same short name is used in all three.
  • Errors are PermDockDeniedError, PermDockApprovalRequiredError and PermDockValidationError. Problem Details type URIs end in /denied, /approval-required and /validation.

Open questions

  • Whether decide should get an explain alias for discoverability.
  • Whether <Protected> should be joined by a second, <Can>-style inline render-prop component.
  • Whether the derived-instance methods should be tenant(id) / team(id) (current) or a single scope({ tenant, team }); the tenancy page carries the rationale for two named methods.

On this page