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
| Name | Kind | Meaning |
|---|---|---|
PermDock | type | The 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). |
permdock | variable | The conventional name for a PermDock instance, and the npm package name. Also the CLI binary (permdock collect). |
createPermDock | function | Exported 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, .filter | One 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 server | Client-side request-access flow; the snapshot's principal, actor and delegation |
<PermDockProvider snapshot endpoint tenant> | <PermDockProvider> from the factory result | Client 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
| Word | Used for | Never used for |
|---|---|---|
definePermissions | Build the reference tree from resources and groups | Rules |
resource | One 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 |
actions | Instance-level actions (can(permissions.post.update, post)) | Type-level checks |
collection | Type-level actions (can(permissions.post.create)) | Instance checks |
mergePermissions, listPermissions, findPermission | Registry helpers; functions so they never collide with resource names | Methods on the tree |
definePolicy | Bind roles, scopes, subject, context, validate to a definition | Defining permissions |
role | A named array of grants with an optional scope ({ on: 'tenant' | 'team' | resource reference, assignable }); same-named roles merge | Postgres roles |
scopes | The 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, on | The three shapes of a Membership (tenant role, team role, resource role) and the on option of role | org, organization, workspace, group as identifiers; those are provider vocabulary the mapper translates |
Membership, CustomRole | The wire types for a scoped role assignment and a tenant-defined role composed of declared assignable roles | Classes; both are plain JSON |
MembershipSource, RoleSource | The two subject-input interfaces (membershipsFor, rolesFor / assignable) passed as memberships and customRoles to every createPermDock | Stores; PermDock never writes a membership or role |
SubjectResolver | The generic type of every subjectFrom* function: verified input in, Subject out, never throws | A class hierarchy |
memoryRoleSource | The in-process RoleSource over a static CustomRole[] | Production storage |
allow, deny | The two grant constructors; both accept one reference or an array | Checking |
subject | Both the reference object used in conditions (subject.id) and the definePolicy option that maps a user to principal values | The agent (that is actor) |
principal, actor, delegation | The three parts of a subject | Synonyms for user or role |
granted, denied, approval-required | The three Decision.outcome values | not-applicable, allow, deny |
where, check | Conditions on the current row and on the next row | Query building outside conditions |
key, scope | The dotted (post.update) and colon (post:update) string forms of a permission | Public 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 claims | Verifying, logging in, or anything that can throw |
createJwtSubjectResolver | The cached, reusable form of subjectFromJwt (one per issuer, JWKS cache); options claims.roles / groups / entitlements / tenant / memberships and groupRoles | A second createPermDock |
<provider>RoleSource | A provider's RoleSource implementation (betterAuthRoleSource) | A subject mapper |
describe | describe(decision) returning { kind, title, detail, alternatives } for tooltips and Problem Details | Logging; it is pure |
ApprovalStore, memoryApprovalStore, approvalsHandler | The pluggable store behind approval-required, its in-process default, and the Fetch routes for approvers (permdock/approvals) | Deciding; a store never influences decide |
DecisionSink, memorySink | The pluggable destination for on('decision') events and its in-process default | Blocking a decision; sinks are fire-and-forget |
store, sink, snapshots | The three options every adapter's createPermDock accepts for an ApprovalStore, a DecisionSink and a SnapshotSource | Anything else; store is never a database handle |
memberships, customRoles, tenant | The three tenancy options every adapter's createPermDock accepts: a MembershipSource, a RoleSource, and how the active tenant is resolved from the request | Reading a tenant from an unsigned header or a model argument |
cloud | cloud({ url, key }) from permdock/cloud, returning approvals, sink and snapshots for PermDock Cloud | A factory for a PermDock; there is no decide on it |
PermDock-Approval | The HTTP request header carrying an approval token on a retried request | Anything 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:
| Word | Why it is reserved |
|---|---|
dock | Early sketches used it as the instance name; it reads oddly next to PermDock and collides with nothing meaningful. Only permdock is used. |
ability | CASL's noun; using it would imply CASL semantics (subject detection, manage/all) that PermDock does not have. |
can as a definer | CASL 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 members | Kilpi 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. |
explain | Replaced by decide, which returns the full Decision rather than a message (ADR 0007). |
Can, Guard, Access | Component names already used by CASL, older sketches and Kilpi; the PermDock component is <Protected>. |
org, organization, workspace, account, group as PermDock identifiers | Every 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, TenantProvider | The tenant is a property of the one snapshot, not a second provider; useTenant and useMemberships read it. |
hasRole, isAdmin, roleGuard | Role 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 path | Returns | Notes |
|---|---|---|
permdock | PermDock | await 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(andreact-native,vue,svelte,solid) exportPermDockProvider,usePermDock,usePermission,usePermissions,useFilter,useTenant,useMemberships,useRoles,useAssignableRoles,useApproval,useSubjectand<Protected>directly.permdock/better-auth,permdock/clerkandpermdock/supabaseexport theirsubjectFrom*mapper and, where the provider stores roles, a<provider>RoleSource; both are values you pass to a factory.permdock/webmcpexportsregisterTools(document.modelContext, group, { permdock })because it consumes a clientPermDockrather than creating one.permdock/drizzle,permdock/prismaandpermdock/kyselyexporttoWhere.permdock/jwtexportssubjectFromJwtandcreateJwtSubjectResolverbecause it produces a subject for a factory, not an instance.permdock/approvalsexportsmemoryApprovalStoreandapprovalsHandler, andpermdock/cloudexportscloud; both produce values you pass to a factory (store,sink,snapshots) rather than aPermDock.- Build hooks live in
@permdock/cli, not inpermdock:permdock/next/pluginexportscreatePermDockPluginand@permdock/cli/unpluginexportscreatePermDockUnplugin(Vite, Rollup, webpack, Rspack, esbuild through unplugin). Both runcollectat build time and never wire runtime API (0006). Thecreateprefix and thePermDocknoun follow the factory rule; the suffix names the host (Pluginfor Next's config API,Unpluginfor the bundler family).
Files and folders
- The server factory lives in
src/permdock/server.tsin every example. The definition issrc/permissions.ts; the policy issrc/policy.ts; generated definitions aresrc/permissions.generated.ts. - Example apps are
apps/examples/<adapter>; the docs page is/docs/adapters/<adapter>; the source folder ispackages/permdock/src/<adapter>. The same short name is used in all three. - Errors are
PermDockDeniedError,PermDockApprovalRequiredErrorandPermDockValidationError. Problem DetailstypeURIs end in/denied,/approval-requiredand/validation.
Open questions
- Whether
decideshould get anexplainalias 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 singlescope({ tenant, team }); the tenancy page carries the rationale for two named methods.