# Naming

Source: https://permdock.com/docs/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 three core names [#the-three-core-names]

| Name | Kind | Meaning |
| --- | --- | --- |
| `PermDock` | type | The immutable, request-scoped decision object returned by `createPermDock`. Methods: `can`, `decide`, `assert`, `explain`, `filter`, `pick`, `where`, `simulate`, `snapshot`, `on`, `actions`; tenancy: `tenant`, `team` (derived instances), `derive` (the same subject with other `customRoles`, `approvalPolicies` or `relations`), `memberships`, `tenants`, `heldRoles`, `audiences`, `assignableRoles`, `assignablePermissions` (read-only), `decideRoleChange`; graph: `loadRelations`, `whoCan`. Vocabulary trees: `permissions`, `roles`, `plans`. |
| `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. |

```ts
import { createPermDock } from "permdock"; // core: PermDock
import { createPermDock } from "permdock/next"; // Next.js: { getPermDock, getPermission, getSnapshot, requireAccess, 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 [#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 |
| `<Protected>` | `await requireAccess({ permission, data?, tenant? })` | Renders children or a fallback on the client; on the server, the granted `Decision`, or `forbidden()` / `unauthorized()` |
| `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()`, `useAssignablePermissions()` | `(await getPermDock()).tenants()`, `.memberships()`, `.heldRoles()`, `.assignableRoles()`, `.assignablePermissions()` | The active tenant with `switchTo`; the membership list; `Role[]` held in a tenant; `Role[]` the subject may hand out; `Permission[]`, the custom-role ceiling the subject may hand out |
| `useApproval(decision)`, `useSubject()` | the [`ApprovalStore`](/docs/adapters/approvals) 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](/docs/concepts/ui)). Vue keeps the `use*` composable names. **Svelte does not:** it exports `setPermDock` / `getPermDock` and readable stores (`permission`, `permissions`, `filter`, `tenant`, …) because Svelte has no React-style hooks. Solid uses accessor functions. The [UI parity table](/docs/adapters) lists them.

## Definition and policy vocabulary [#definition-and-policy-vocabulary]

| Word | Used for | Never used for |
| --- | --- | --- |
| `definePermissions` | Build the reference tree from resources and groups | Rules |
| `renamed`, `formerKeys`, `renamedFrom` | The `definePermissions` option mapping former keys to current keys, the function that lists a leaf's former keys, and the catalog field that carries them | Keeping an old leaf alive; `aliases`, `legacyKeys`, `deprecated` |
| `defineRoles`, `definePlans` | Build the typed `Role` and `Plan` trees (`permdock.roles.admin`, `permdock.plans.pro`) | Grants; Postgres roles |
| `Role`, `Plan` | Frozen vocabulary leaves `{ key, on?, assignable, meta }` and `{ key, meta }`. A role named only by string becomes a synthesised `Role` leaf with `assignable: false` | The grant-list type; that is `RoleBinding` |
| `RoleMeta`, `audience` | A role's `meta`: `ActionMeta` plus `audience`, the surface its holders use (`'staff'`, `'portal'`, `'platform'`); `permdock.audiences()` and `snapshot.audiences` list the distinct values of the roles held in the active tenant | A per-surface role list; a hard-coded role name in a layout |
| `min`, `max`, `transferOnly`, `assigns`, `for` | Role ownership options: holders kept per scope instance, holders allowed, a holder count that only moves by transfer, the roles a holder may assign and revoke, the membership kinds (`via`) that may hold the role ([ownership](/docs/concepts/ownership)) | `minHolders`, `canAssign`, `allowedVia`; `activation` (reserved for time-boxed roles) |
| `decideRoleChange`, `RoleChange`, `RoleChangeDecision` | `permdock.decideRoleChange({ kind: 'assign' \| 'revoke' \| 'transfer', role, scope, id, within?, target, holders? })`, or `scope: 'global'` with no `id` for a global role, and `assignableRoles({ scope: 'global' })`: the ownership rules for one change, answered as `granted` (with the authorising `role`) or `denied` with `denials`; the subject is always the actor | A write; an actor or subject taken from the change |
| `resource` | One resource node: schema, `id` field, `actions`, `collection`, optional `parent` (which may be the resource itself), optional `relations`, optional `version`, optional `restricted`, optional `disclosure` | Anything without a schema-or-actions shape |
| `name` (resource option) | The resource name leaves, tables, relations and AuthZEN types use when the last path segment collides | Changing the key; `alias`, `table` |
| `disclosure` | The `resource()` option `'hide' \| 'reveal'` (default `'reveal'`): with `'hide'`, a denied check on a loaded row answers `404` `/not-found`, the body a missing row gets, and the reason stays in audit | `visibility`, `secret`, a per-grant flag |
| `restricted`, `ResourceRestricted`, `stops` | The `resource()` option naming a boolean column: a row where it is `true` is reached only by grants on itself, never by relations held on its ancestors or through its links; `{ field, stops }` names the paths it closes (`parent` and link names) ([relationships](/docs/concepts/relationships)) | A deny; the reserved `role(..., { restricted })` option |
| `FieldRelation`, `EdgeRelation`, `PrincipalRelation` (`edge`, `object`, `subject`, `expiresAt`, `principal`, `period`, `startsAt`) | The three relation shapes on a resource: a row field holding the principal id (`{ field, memberOf? }` or a string), an edge table (`{ edge, object?, subject?, expiresAt? }`), and a principal column with an optional validity `period` (`{ principal, period?: { startsAt?, expiresAt? } }`) | A tuple store; a relation declared on a grant |
| `through`, `depth` | `relation(resource, name, { through: 'parent', depth })`: follow the row's parent chain upward to `resource`, then its self-parent for up to `depth` hops (default 16, at most 32) | `recursive`, `levels`; walking a `memberOf` relation |
| `RelationSource`, `RelationChain`, `RelationHolder`, `memoryRelations`, `relations` | The object-graph interface (`ancestors`, `related`), its answers, its in-process default over rows and edges, and the `createPermDock` option that takes it | A module-level cache; a decision |
| `loadRelations`, `whoCan`, `WhoCan`, `Holder`, `HoldingVia` | `await permdock.loadRelations(permission, rows)` fills the instance's relation cache for an async source; `await permdock.whoCan(permission, row)` lists holders and how (`role`, `relation`, `share`) with `complete` | A grant; a partial list presented as complete |
| `related`, `RelatedCondition`, `RelationGrantee`, `RestrictedAncestors`, `restrictedAncestors`, `passRestricted` | The condition node a graph relation compiles to, and the relation grantee type with `through` / `depth` | A hand-written `where` |
| `permdock_closure`, `permitted_<resource>_ids`, `permdock_closure_<resource>`, `permdock_restricted_<resource>` | The closure table, the per-resource helper keyed by relation name and the trigger-maintained refresh function `permdock rls generate` emits for graph grants, and the helper listing the rows a closed link does not reach below | A table PermDock writes at runtime; `service_role` |
| `actions` | The `resource()` option listing instance-level actions (`can(permissions.post.update, post)`), and the instance verb `permdock.actions(permissions.post, post)` returning one result per leaf of a resource | Type-level checks |
| `collection` | Type-level actions (`can(permissions.post.create)`) | Instance checks |
| `crud`, `readable`, `writable` | Option factories for `resource()`: conventional instance/collection split and default `meta` | Constructors; grants; a default of `resource()` |
| `mergePermissions`, `listPermissions`, `findPermission` | Registry helpers; functions so they never collide with resource names | Methods on the tree |
| `parseCatalog`, `rowConditionKeys`, `catalogPath` | `permdock/catalog` reads and freezes a `permissions.catalog.json` and lists its `rowConditions: true` keys; `permdock/cli` resolves the file path the way `collect` does | `loadCatalog`, `readCatalog`; a reader that returns `null` instead of throwing `PermDockValidationError` |
| `compileWhere`, `CompiledWhere` | `permdock/compile` lowers a portable condition, for one subject, to the tree the ORM `toWhere` compilers render | A policy-time compile with no subject; RLS and PowerSync compile the condition themselves |
| `createServerKernel`, `ServerKernel`, `ServerKernelOptions`, `tenantScope`, `TenantScope`, `TenantOption` | `permdock/server`: the kernel an HTTP adapter builds on. It hands out no instance, so it is not `createPermDock`. `tenantScope` resolves an adapter's `tenant` option against its framework context. | `createPermDock` for an adapter, which would return no instance |
| `isPermission` | The structural guard for a permission leaf (string `key`, `scope`, `resource`, `action` and an object `meta`), which also accepts a leaf that crossed JSON and lost its non-enumerable `kind` | A registry lookup; identity stays by `key` |
| `definePolicy` | Bind vocabulary, `scopes`, `grants`, `principal`, `context`, `validate` to a definition | Defining permissions |
| `role` | A `RoleBinding`: named array of grants with an optional scope (`{ on: <declared scope name> \| resource reference, assignable }`, typed against `definePolicy({ scopes })`), the ownership options `min`, `max`, `transferOnly`, `assigns`, `for`, `exclusiveWith` and a `RoleMeta` `meta`; `activation` and `restricted` are reserved; same-named bindings merge; first argument may be a `Role` leaf | Postgres roles; the only grantee |
| `to`, `anyone`, `authenticated`, `relation`, `plan`, `actor`, `assurance` | Grantee selectors on `allow` / `deny`. `anyone()` is the only selector that evaluates for `principal: null`; an array in `to` means every selector must match | A second permission check; `hasRole` |
| `scopes` | The `definePolicy` option declaring named scopes in order, each with its row `key` and an optional parent `within` (`{ organization: { key: 'organization_id' }, customer: { key: 'customer_id', within: 'organization' } }`) ([named scopes](/docs/concepts/scopes)) | Permission scopes (`.scope` is the colon string form) |
| `within` | A scope's parent in `definePolicy({ scopes })`, and on a `Membership` the ids of its ancestor scopes | `parent` (that is the resource chain) |
| `scope`, `id`, `on` | The two shapes of a `Membership` (an instance of a named scope, a resource role) and the `on` option of `role` | Hard-coded tenant and team kinds in core |
| `tenant`, `team` | Aliases of the first and second declared scope (in `role` `on`, `memberOf`, and the `{ tenant, team }` membership input form); `principal.tenant` is the active instance of the first scope | Scopes named after a provider's vocabulary when the policy's own names differ |
| `Membership`, `CustomRole`, `CustomRoleGrant` | The wire types for a scoped role assignment, a tenant-defined role (`includes` declared roles, `grants` of declared permission keys, optional `scope` and `id`) and one of its `{ permission, effect? }` entries | Classes; a condition, approval or limit on a custom-role grant; all are plain JSON |
| `customRoleClaim` | Builds the compact `memberships[].grants` map (`key`, `-key`, `@role` per custom role name) that RLS helpers read in `jwt` mode with `--custom-roles` | A claim PermDock trusts without `subjectFromJwt`; a per-provider helper |
| `custom_role_permissions`, `custom_role_includes`, `permdock_ceiling`, `permdock_custom_keys` | The SQL objects `permdock rls generate --custom-roles` emits next to `role_permissions` and the helpers | Tables PermDock writes at runtime; a way around the ceiling |
| `resolveCustomRole`, `validateCustomRole` | The one resolver of a custom role against the ceiling of assignable declared roles (`{ grants, dropped }`), and its summary `{ ok, permissions, dropped }` for a save action and `permdock doctor` PD023 ([custom roles](/docs/concepts/custom-roles)) | Widening a role; resolving a name a declared role already uses |
| `CustomRoleDrop`, `CustomRoleDropReason` | Why a custom-role entry was left out: `unknown-permission`, `outside-ceiling`, `condition-not-allowed`, `unknown-level` (with `level`), or `{ role, reason: 'unknown-role' }` for an include | Denial reasons; a drop is never a `Decision` |
| `meta.readOnly`, `meta.destructive`, `meta.idempotent` | Action metadata the MCP adapters turn into `readOnlyHint`, `destructiveHint` and `idempotentHint` for tools whose author set no annotation | A decision input; `tags: ['destructive']` |
| `meta.x`, `MetaValue` | Application-owned JSON on an action's metadata; PermDock carries it and never reads it | A decision input; `meta.custom`, `meta.extra` |
| `x` (definition option), `AppData`, `AppDataSchema`, `DefinePermissionsOptions`, `VocabularyOptions`, `PolicyAppDataSchemas` | One Standard Schema per kind of app data: `definePermissions(…, { x: { permission, resource } })`, `defineRoles(…, { x })`, `definePlans(…, { x })`, `definePolicy(…, { x: { grant, membership } })`; `AppData` is the default plain JSON object | A decision input; `declare module` augmentation; `extensions`, `custom` |
| `ResourceMeta`, `GrantMeta` | A resource's `{ title?, description?, x? }` and a grant's `{ description?, x? }`; carried to decisions, events, snapshots and the catalog | Policy fingerprint input |
| `ResourceXTree`, `RESOURCE_X` | The type `definePermissions` adds to a tree so `getResource` types `meta.x`; `RESOURCE_X` is a type-only key with no runtime value | A runtime property |
| `Membership.x` | Application-owned JSON on a membership, from a trusted source; dropped with an `on('auth')` `schema` event when invalid; never in claims | A condition input; `metadata`, `attrs` (that is the token claim) |
| `obligations`, `AppObligation`, `ObligationInput` | `allow(p, { obligations })` and the `{ kind: 'app', name, detail? }` entries it puts on a granted decision | A new `kind` per app need; obligations on a deny |
| `DenialDetails` | The map from a denial reason to its typed `detail` (`limit`, `inactive-grant`, `deny`), read through `Denial<R>` | A free-form detail per reason |
| `onDenied` (adapter option), `OnDenied`, `OnDeniedEvent` | The HTTP adapter hook that receives `{ decision, problem, request }` for a refusal and returns Problem Details, a `Response` or nothing; it cannot grant | `onForbidden`, `errorFormatter`; a hook that turns a denial into a grant |
| `OnDeniedText`, `DeniedTextEvent` | The MCP and AI SDK `onDenied` hook, which may replace only the refusal text | A hook that changes `isError` or `structuredContent` |
| `context` (adapter option), `RequestContext` | The adapter hook that returns server-derived JSON merged into `subject.context` for the request | A subject, membership, tenant or actor source; `locals`, `extra` |
| `wrap`, `wrapPermDock`, `PermDockWrap`, `PermDockOverrides` | The adapter option that wraps every request's instance after `otel`, and the helper that overrides methods and keeps `tenant()`, `team()` and `derive()` wrapped | A plugin system; `middleware`, `decorate` |
| `field`, `explain` (`protect` options) | The per-route field check and decision trace passed to `decide` | A field mask; a trace on the wire |
| `defaults`, `PermDockDefaults` | Provider-wide content for the `<Protected>` slots a component leaves out | `fallbackComponent`; a global mutable default |
| `useDescribe`, `getDescribe` | `describe` with the provider's `messages`; `getDescribe` in Svelte | A second description format |
| `meta.manageRoles` | On a `Role` or a permission leaf: holding the role, or being granted the permission, makes the full ceiling assignable instead of only what the subject holds | A grant; a bypass of `RoleSource.assignable` |
| `SnapshotAssignable` | One `{ tenant, roles, permissions }` entry of a snapshot's `assignable` list | A second snapshot format |
| `SnapshotScope` | One `{ name, key, within?, resources? }` entry of a snapshot's ordered `scopes` list | A tenant-and-team pair of keys |
| `Scope`, `ScopeDeclaration`, `PolicyScopesInput`, `PolicyScopes`, `ScopeNames` | A declared scope `{ name, key, within? }`, one `definePolicy({ scopes })` entry `{ key, within? }`, the whole option, `Policy.scopes` in order, and the names `on` accepts (declared names plus the `tenant` / `team` aliases) ([named scopes](/docs/concepts/scopes)) | A scope kind enum |
| `GrantScope`, `RoleScope` | Where a grant applies (`'global'`, a scope name, or `{ resource }`) and what `role(..., { on })` accepts | Hard-coded `'tenant' \| 'team'` |
| `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 |
| `memoryMembershipSource` | The in-process `MembershipSource` over memberships keyed by principal id | Production storage |
| `memorySnapshotSource` | The in-process `SnapshotSource` over one snapshot; `set` replaces it and calls each subscriber | Production storage |
| `customRoleSource`, `CustomRoleReader`, `CustomRoleSourceOptions` | A `RoleSource` over `rolesOf(tenant)`, a read of every custom role of the tenant; `read: 'held'` skips the read when the subject holds only declared roles | A source that returns only the held roles; `rolesForUser` |
| `globalRoles`, `scope: 'global'` | The `RoleSource` method returning platform custom roles, and the custom role scope that marks one | A tenant role without a tenant; `platformRoles`, `systemRoles` |
| `ApprovalPolicySource`, `ApprovalPolicy`, `memoryApprovalPolicies`, `approvalPolicies`, `validateApprovalPolicy`, `ApprovalPolicyProblem` | Approval requirements kept as data, the entry shape, the in-process default, the `createPermDock` option, and the check that names why an entry does not load (`unknown-permission`, `invalid`, `where-on-collection`, `stale-on-without-version`); entries only add stages | Granting or relaxing an approval; `approvalRules`, `workflows` |
| `composeMemberships`, `claimsFirst` | One `MembershipSource` over several (merged, de-duplicated, `list` and `version` combined); a source that keeps the verified token's memberships until the token says they were truncated. An array passed as `memberships` is composed | A store; a cache with its own TTL |
| `MemberEntry`, `list`, `version`, `claimsFirst` | The optional `MembershipSource` members: every member of one scope instance (`{ principal: { id }, membership }`), the principal's authorization version, and the claims-first flag | `members`, `epoch`, `revision` |
| `EntitlementSource`, `entitlementsFor`, `entitlements`, `memoryEntitlementSource`, `fromStripeEntitlements`, `testEntitlementSource` | The subject-input interface for plan names per tenant, its `createPermDock` option, the in-process default, the Stripe Entitlements reader over a structural client, and its conformance runner | A `permdock/stripe` entry; `plans` as an option name |
| `fresh`, `stale`, `stale-credentials`, `authzVersion`, `authz_ver` | The `definePolicy` option listing permissions that need an up-to-date token, the subject flag, the denial reason, the `permdock/supabase` version reader and the claim it compares | `sensitive`, `strict`, `stale-token` |
| `onStale` | The `claimsFirst` option for a token whose authorization version is behind: `'deny'` (default) denies `fresh` permissions, `'reread'` reads the memberships from the sources instead | A re-read on every request |
| `managedBy: 'idp'`, `isExternallyManaged`, `externally-managed` | A membership the identity provider owns, its predicate, and the `decideRoleChange` reason that refuses to change it | `readonly`, `locked`, `scim: true` |
| `fromSupabasePostgres`, `SupabasePostgres` | `ctx.postgres` or `ctx.postgresAdmin` from `@supabase/server`'s `withPostgresClient` as a `SqlQuery`, through its `queryRaw` | A Postgres client; a new connection |
| `fromTable`, `fromJunction`, `SqlQuery`, `SqlMembershipSource`, `RoleThrough`, `supabaseMembershipsBudget` | The `permdock/supabase` SQL membership sources, the statement runner they take, their type, a role or user column read through another table, and the default claim budget in bytes | A query builder; an ORM adapter |
| `postgrestSources`, `subject_for`, `authz_version_for`, `members_of`, `SupabaseRpcClient`, `SupabaseRpcCaller`, `SubjectRecord` | The `permdock/supabase` sources over PostgREST: one `subject_for(p_user)` call per user, generated next to the hook, gives `memberships`, `customRoles(principal)` and `subject(userId)` for a backend that only has supabase-js, `authz_version_for(p_user)` gives `memberships.version` without the full read, and `members_of(p_scope, p_id)` gives `memberships.list`; `SupabaseRpcCaller` is the client parameter, loose enough for a supabase-js client typed with a generated `Database` | A client-callable function; a subject from a request body |
| `RoleSourceFactory` | `(subject) => RoleSource \| undefined`, the per-subject form of every `customRoles` option, called once per instance with the resolved subject | A source that remembers the last principal it served |
| `permittedIds` | The instances of a scope where the subject holds a permission with no row condition, minus denied ones, or with `conditioned: true` where a conditioned allow applies: the in-process mirror of `permitted_<scope>_ids_by_permission`; a listing, never a decision | A decision; a check of one row |
| `countHolders` | The live holders of a role in one scope instance, from `MembershipSource.list`, for `decideRoleChange`'s `holders` | A holder count from a second, unfiltered query |
| `operationPermissions`, `operationPermissionsFromOpenApi` | One declaration of each API operation's permission, `"METHOD /path/{param}"` to a permission, read by an HTTP gate (`forRequest`) and by `permdock/mcp`'s `permissionFor` (`forOperation`) | A regular-expression route table; a second mapping for the MCP tools |
| `oauthScopes` (MCP tool config, `protect` options, an operation entry, `withPermDock` config), `oauthScopesFor`, `oauthScopesForRequest`, `oauthScopesForOperation`, `operations`, `ScopeGuard` | An operation's own OAuth scopes in place of those derived from its permission, narrowing which coarse scope reaches each MCP tool or REST route that shares a permission, or the only gate of a route that checks no permission (`protect(null)`, `ScopeGuard`); `operations` on `permdock/server` reads them from `operationPermissions` | A second permission only to tell two tools apart |
| `supabaseApprovalStore`, `rls.approvals`, `approval_requests`, `permdock_approval_*` | The generated approval store (a table and one function per `ApprovalStore` method, executable by no client role) and the `permdock/supabase` store over it | An approval table a client role can read or write |
| `rls.jsonSchema`, `approval_requests_body_schema` | `'auto' \| boolean`: a pg\_jsonschema check constraint that each approval `body` matches `approval-request-v1.json` | A remote `$ref`; `format` assertions |
| `APPROVAL_POLICY_UNAVAILABLE` | The `approval-policy-unavailable` denial detail when an `ApprovalPolicySource` fails, as a constant | A second spelling of the detail |
| `supabaseClaims`, `SupabaseClaims`, `SupabaseMembershipClaim`, `SupabaseClaimsSchema`, `extend` | The `permdock/supabase` Standard Schema of the hook's claim contract (loose, no validation library), its output types, and the method that merges an app's own claim schema over it | `supabaseClaimsSchema`, `extendClaims`; a strict object that rejects other claims |
| `oauthScopes`, `PolicyOAuthScope` | Coarse OAuth scopes an authorization server issues (`mcp:read`), each covering permissions; expanded into the token delegation and named in scope challenges | `scopeAliases`, `aliases`, `scopeMap` |
| `ClientNames`, `clients`, `actor.client`, `to: { kind, client }` | Stable names for OAuth clients whose ids differ per environment or come from dynamic registration: the subject resolver maps a verified client id to a name, and a policy delegation names the client instead of the id | `aliases`, `clientAlias`, a client id in the policy |
| `actorOf`, `delegationOf`, `SupabaseActor`, `SupabaseActorResult` | The `permdock/supabase` rules that turn `act` / `client_id` into the acting party (`oauth-client`, `support`, `impersonation`, by `act.kind`) and `scope` into delegated scopes; `{ ok: false, reason: 'invalid-chain' }` must deny | `getActor`, `parseActor`; returning `null` for a malformed chain; `admin` or `impersonator` as an actor kind |
| `anonymousSignIns` | `SupabaseSubjectOptions` option: `'deny'` maps a token with `is_anonymous: true` to the anonymous subject, as `rls.anonymousSignIns` does in RLS | `allowAnonymous`, a boolean |
| `liveSession` | `SupabaseSubjectOptions` option and `Subject` field: `true` once the caller checked the session against the Auth server for this request; never read from claims | `fresh`, which `fresh` permissions already name; `sessionValid` |
| `supabaseClaimVectors` | `permdock/testing` export: `supabaseClaimFixtures` as a `supabase/sdk` conformance vector file, `{ feature: "auth.session.get_claims", cases: [{ name, input, expected }] }` | `supabaseConformance`, which names no feature; a JSON file outside the package |
| `requires` | Supabase manifest field: `{ matrix, capabilities }`, the capability-matrix release tag and the feature ids the setup depends on | `capabilities` at the top level, which reads as what PermDock offers |
| `pageApprovals`, `approvalPageSize`, `encodeApprovalCursor`, `decodeApprovalCursor`, `listAllApprovals`, `ApprovalCursorPosition` | The list-paging helpers the built-in approval stores use, exported from `permdock/approvals` for a custom `ApprovalStore` ([approvals](/docs/adapters/approvals)) | A cursor format of the store's own; `paginate`, `listAll` |
| `{ subject: { session: { live: true } } }` | The `where` form for a grant that needs a live session; normalised to `{ "op": "liveSession" }` | A `principal.sessionLive` ref, which would read a claim |
| `Actor.readOnly` | `true` narrows every policy delegation to that actor to its read-only permissions (`readOnlyHint`); a Supabase support session sets it from `act.read_only` | `readonly`, `writable: false` |
| `supabaseTenantClaim` | The default claim (`tenant_id`) for the active tenant, shared by the hook, `subjectFromSupabase` and the RLS helpers | A per-app tenant name; a column name |
| `SupabaseHookManifest`, `SupabaseManifestMembership`, `SupabaseManifestRls`, `SupabaseManifestHelper`, `SupabaseManifestValue`, `SupabaseManifestColumn`, `supabaseHookManifestFixture`, `permdock supabase inspect`, `permdock.manifest.json` | The `version: 1` manifest of the generated hook and SQL helpers (helper names and signatures, tenant claim, budget, claims written, membership sources, RLS mode, deciding columns, marker majors), its types in `permdock/supabase`, its fixture in `permdock/testing`, and the file `inspect --out` writes | `apiVersion`; a second hook generator |
| `SupabaseManifestRole`, `SupabaseManifestThrough` | A manifest membership's role: a column, fixed roles, or a column with the roles table (`table`, `id`, `column`) it references; a membership's `user` takes the same `through` | `apiVersion`; a second hook generator |
| `permdock supabase hook generate` | The CLI command that compiles the SQL sources into the Custom Access Token Hook | `permdock hook`, `permdock rls hook` |
| `attrs` (`supabase.hook.attrs`, the claim) | Allow-listed server-owned columns and `app_metadata.<key>` entries the hook writes for attribute conditions (`principal.claims.attrs.<key>`) | `profile`, `user_metadata`, `claims` as a config key |
| `supabase.hook.api` | The PostgREST wrappers `<prefix>subject_for`, `<prefix>members_of` and `<prefix>authz_version_for` (default schema `public`, prefix `permdock_`) that `hook generate` writes for a backend reaching Postgres through the Data API; `postgrestSources({ api })` takes the same setting. The one place the hook grants to `service_role`, and only when the option is set | A hand-written wrapper per function; a `grant` to `authenticated` |
| `supabase.hook.before` | Schema-qualified functions `(event jsonb) returns jsonb` the generated hook calls first, in order; a result with `error` is the hook's answer | A second access token hook; a check that writes the claims PermDock owns |
| `supabase.hook.validate` | Check the claims the hook wrote against `supabase-claims-v1.json` with pg\_jsonschema and drop them all on a mismatch | Raising an Auth error on a bad claim; validating claims another package owns |
| `LimitStore`, `memoryLimitStore`, `limits` | The quota counter for `limit` grants, its in-process default, and the `createPermDock` option (core, every HTTP and agent adapter) | Auto-installing a store; a thenable `consume` as granted |
| `GrantLimit`, `mode`, `alertAt` | The `limit` grant option `{ count, per, mode?, alertAt? }`: `mode` is `'hard'` (default) or `'soft'`, `alertAt` a fraction of `count` in `(0, 1]` ([limits](/docs/concepts/policies#limits)) | A soft limit that grants when the store is unreachable |
| `Quota`, `quota` | `{ remaining, resetsAt }` on a `granted` decision under a `limit`; `resetsAt` is Unix seconds | A snapshot field; a denial |
| `LimitDetail` | `{ count, window, resetsAt }`, the `detail` of a `limit` denial: the grant's count, its window in seconds and its reset in Unix seconds; HTTP adapters render it as `429` with `Retry-After`, `RateLimit` and `RateLimit-Policy` | `quota` on a denied decision; a store read in the renderer |
| `not-entitled`, `requiredPlans`, `SnapshotNotEntitled` | The denial reason when a held role's grant fails only on its `plan` grantee; `requiredPlans(decision)` lists the plan keys when every denial is `not-entitled`; `describe` kind `upgrade` with `plans`; 403 `/not-entitled` with `plans`; the snapshot's `notEntitled` entries `{ permission, role, to }` | `no-plan`, `upgrade-required`, `missing-entitlement`; a `402` |
| `Obligation`, `obligations`, `over-limit`, `near-limit` | What a `granted` decision says the caller owes, as `{ kind }` objects; the kinds so far are `over-limit` (a soft limit past its count) and `near-limit` (usage reached `alertAt`) | A fourth outcome; an obligation on `denied` or `approval-required` |
| `pdp` | The HTTP adapter and kernel option taking `createPermDock` from `permdock/pdp`, so `protect` decides delegated permissions remotely | Making the request-scoped instance async |
| `ProtectOptions`, `trusted` | The third argument of every `protect` (`{ trusted: true }` skips validating what the loader returns) | A per-route `validate` mode |
| `scope` (decide option) | The `DecideOptions` field naming a declared scope whose memberships alone answer a check (`can(permission, undefined, { scope: 'organization' })`); global roles still apply ([scopes](/docs/concepts/scopes#collection-checks)) | `scope(...)` as a derived instance, `tenantOnly`, `firstScope` |
| `request` | The `permdock/trpc` and `permdock/orpc` option mapping a context to its Web `Request`, and the `permdock/nest` option mapping a non-HTTP `ExecutionContext` | Reading a subject from it directly |
| `problemFromError` | The `permdock/server` function turning a thrown PermDock error into its Problem Details `Response`, `undefined` for anything else | An error class; a catch-all that maps non-PermDock errors |
| `ormParity`, `OrmParityScenario` | The `permdock/testing` runner comparing `filter()` in memory with the rows a database returns for `toWhere(where())`, and its scenario shape | A database the runner opens itself |
| `testClientParity`, `ClientParityCase` | The `permdock/testing` runner asserting that `fromSnapshot` never grants what the server denies, per membership and tenant, and its case shape | Deriving expectations from the snapshot itself |
| `testClientStore`, `ClientStoreFactory` | The `permdock/testing` runner for client snapshot stores and the factory it calls ([scenario testing](/docs/guides/scenario-testing)) | A DOM or a framework renderer |
| `WebMcpToolCall` | The `{ input, token }` argument a `permdock/webmcp` handler receives | Positional `input` arguments |
| `listFields`, `requiredFields` | `toWhere` options: scalar-list fields whose `contains` is element membership (Prisma, Kysely), and Prisma's non-nullable fields, where a null branch folds away | Inferring either from the Prisma client at runtime |
| `prismaModelFields`, `PrismaModelFields`, `model` | The `permdock/prisma` function reading required and list fields from `schema.prisma` text or a DMMF datamodel, its result, and the `toWhere` / `toPredicate` option taking it | Loading the schema file itself |
| `toPredicate` | The `permdock/prisma` compiler for Prisma 8 field-proxy predicates | A second `toWhere` overload |
| `checkRow`, `RowCheck` | The single-row check of `permdock/drizzle`, `permdock/kysely` and `permdock/prisma`, and its `{ found: false } \| { found: true, granted }` result | A `not-found` outcome on `Decision` |
| `resolveRelated` | The `permdock/prisma` step replacing `related` nodes in a `where()` result with ids read through a raw query | A compiler; it returns a `WhereResult` |
| `relations` (compiler option), `RelationsMapping` | Where the graph tables live for Drizzle and Kysely `toWhere` and `resolveRelated`: `tables` and `closure` | The `createPermDock` `relations` source, which answers facts in process |
| `match`, `groups`, `includes`, `links` | Relationship declarations: an edge table's fixed column filter, its group holders, relations implying this one, and named to-one references walked by `through: [...]` | Tuple rewrites; `through` naming a field |
| `MemberOfParent` | A `memberOf` `parents` entry: a keyed `{ field, resource }` or a bare field name | Walking a parent chain with a query |
| `WhereResult.subject` | The non-enumerable subject a `where()` result carries so `toWhere` compiles `memberOf` without a second argument | A serialised field; it never reaches JSON |
| `testAuthZen`, `AuthZenVectors`, `authzenTodoPolicy`, `authzenTodoVectors` | The `permdock/testing` AuthZEN interop runner, its vector layout (the interop harness files unchanged) and the interop Todo domain (`authzenTodoPermissions`, `authzenTodoUsers`, `authzenTodoData`) | Vendoring the official vectors |
| `testHttpAdapter`, `HttpMounted` | The `permdock/testing` HTTP scenario runner and the value a mount returns (`fetch`, `call`, `close`) | A server the runner starts itself |
| `ArazzoSimulateInput`, `ArazzoPlan`, `arazzoFindings` | The `simulate({ arazzo, openapi })` argument and result, and the resolution-only helper the CLI uses | A `permdock/arazzo` package; a subject on the call |
| `webBotAuth`, `verifyWebBotAuth`, `discoverViaSignatureAgent` | The HTTP `createPermDock` option for RFC 9421 signatures, the verifier it calls (`(request) => verifyWebBotAuth(request, options)`, so an app without it bundles no verification) and its `Signature-Agent` directory lookup | A `permdock/web-bot-auth` package; trusting a host that is not on `allow` |
| `InvalidSignatureError` | Thrown by `permdock(request)` when a claimed Web Bot Auth signature fails; `protect` returns the Problem Details `response` instead | Downgrading a failed signature to an anonymous actor |
| `allow`, `deny` | The two grant constructors; both accept one reference or an array and a `to` grantee | Checking |
| `principal` | The condition ref builder (`principal.id`) and the `definePolicy` option that maps a user to principal values. There is no `subject` ref builder; `subject` stays accepted as the `definePolicy` option name | The RFC 8693 `Subject` (that stays `subject` on the instance) |
| `context` | The condition ref builder for values loaded by the `definePolicy` `context` function (`context.teamIds`) | Request-scoped state; the `ctx` a closure receives |
| `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 |
| `sqlFunction` | Named SQL function plus a portable `twin` the evaluator, `filter`, `where` and snapshots run; `rls generate` emits the call | A runtime database round-trip; a closure; inlining the function body as the only representation |
| `opaque` | Imported SQL with a fingerprint and no twin; evaluates to `false` | A portable grant |
| `fields`, `pick` | Schema keys on a grant and the redaction helper `permdock.pick(permission, row)` | CASL globs; a permission per field |
| `validFrom`, `validUntil`, `validity` | The grant options bounding when a grant applies (RFC 3339 or Unix seconds), and the normalised `{ from?, until? }` on `Grant` and a snapshot grant; an inactive allow denies with `inactive-grant` | `expiresAt` (that is a membership's end), `notBefore`, `window`, a clock operator in `where` |
| `group` | The grant option naming a condition group in generated SQL: the grant key is `<permission>#<group>` instead of a positional `#n` | A positional key in hand-written SQL; a group per role |
| `requires` | The allow option naming a permission, or a list of them, the subject must also hold through a role, globally or on the row's scope instance, for the grant to count | `permission()` as a grantee; a `where` on roles |
| `inherit(permission, { through })`, `RelationSource.row` | The grantee that holds on a row when the subject holds `permission` on the row a link list or the parent points to, decided by every grant of that permission; `row` reads that row by id | `from`, `via`, a relation per way the target becomes readable |
| `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`: `subjectFromJwt`, `subjectFromIntrospection`, `subjectFromSupabase`, `subjectFromSupabaseSession`, `subjectFromClerk`, `subjectFromBetterAuth`, `subjectFromMcp`, `subjectFromCapability`, `subjectFromCiOidc`, `subjectFromApiKey`; each provider mapper takes a `schema` option ([authentication](/docs/concepts/authentication)) | Verifying, logging in, or anything that can throw |
| `JwtPrincipal` | The base principal type `permdock/jwt` exports (`id` from `sub`, `issuer` from `iss`, `assurance`, RFC 9068 roles and groups) | A session type; claims stay on `principal.claims` |
| `SupabasePrincipal` | The base principal type `permdock/supabase` exports (`id` from `sub`, roles and `tenant` from hook claims, `memberships`, `assurance.acr` from `aal`) | A session type; `user_metadata` is never read |
| `supabaseRls`, `authorizeSql` | RLS compile hints for `permdock rls` and the `authorize()` function SQL with an optional tenant parameter | A policy builder; it does not decide |
| `suspension` (`RlsSuspension`, `SupabaseSuspension`, `RlsSuspendedScope`, `SupabaseSuspendedScope`, `RlsSuspendedMembership`, `SupabaseSuspendedMembership`) | The `rls` config key, `supabaseRls` and `authorizeSql` option naming the users and scope-instance tables (`RlsActiveRow` / `SupabaseActiveRow`: `table`, `id`, `disabledAt`, `status`, `active`) the generated helpers, `authorize()` and the token hook read to drop suspended rows, and per scope `keep`, the permissions a suspended instance's members still hold, which a membership carries as `Membership.keep`; `memberships.keep` is the same list for a membership whose table's `disabledAt` column is set (`RlsMembershipTable`, `fromTable` `columns.disabledAt`, `fromJunction` `disabledAt`) ([RLS](/docs/cli/rls)) | A `createPermDock` option; in process, suspension is a `MembershipSource` concern |
| `permdock_has`, `permitted_<scope>_ids`, `member_<scope>_ids`, `role_permissions` | The SQL objects `permdock rls generate` emits: `permdock_has(p_grant)` for global roles, one `permitted_<scope>_ids(p_grant)` per declared scope (`permitted_tenant_ids` and `permitted_team_ids` for a policy that declares none), over the seeded `role_permissions (role, permission, grant_key, scope, effect)`, and one membership-only `member_<scope>_ids()` per declared scope, plus `member_<scope>_ids_for(p_user)` on Supabase for a scope with a membership source, executable by `supabase_auth_admin` only, and in `database` mode `permdock_has_for(p_user, p_grant)` and `permitted_<scope>_ids_for(p_user, p_grant)`, executable by no client role; `permdock_has_permission(p_permission)`, `permitted_<scope>_ids_by_permission(p_permission)` and `grant_keys(p_permission, p_scope, p_effect)` take a permission key and answer with its unconditional allows minus any deny; a grant key is the permission key or `permission#n` ([RLS](/docs/adapters/rls)) | Per-row checks; anything a client calls with another user's id; `<scope>_ids_for_user` or `member_<scope>_ids(p_user)` for the per-user helper; `permitted_<scope>_ids()` with no argument or `<scope>_ids_for_member` for the membership-only helper |
| `permdock_role_permissions`, `permdock_permission_keys` | The role and catalog readers `permdock rls generate` emits: `permdock_role_permissions(p_role, p_scope, p_tenant, p_scope_id)` lists the permission keys a role holds with their effect, `permdock_permission_keys()` every declared key and `permdock_permission_keys(p_scope)` those some declared role is allowed on a scope, both executable by `authenticated` ([RLS](/docs/adapters/rls)) | Hand-written joins over `role_permissions`; `role_permission_keys` or `permission_keys` |
| `permdock_trusted_role_permissions`, `rls.trustedReaders` | The role reader for server code without the caller check, and the Postgres roles granted execute on it | A flag on the member-facing reader; `service_role` hard-coded |
| `permitted_<scope>_permission_keys`, `permitted_<scope>_permission_keys_for` | Every permission key the caller (or a named user) holds on one scope instance, as the permission-key helpers answer each key, in one call ([RLS](/docs/adapters/rls)) | A loop of `permdock_has_permission` calls; `held_permissions` |
| `permitted_<resource>_rows`, `permitted_<resource>_rows_for`, `rowHelpers` | The per-resource row helper `rls.rowHelpers` turns on: the row ids the caller (or a named user) may act on with one permission, as the generated policies decide it ([RLS](/docs/adapters/rls#row-helpers)) | `permitted_<resource>_ids` (that is the graph helper keyed by relation); `allowed_rows` |
| `rls.membershipSources` | The `fromTable` / `fromJunction` sources the `database`-mode helpers read for a scope `rls.memberships` maps no table for; default `supabase.hook.memberships` | A second membership mapping per scope |
| `permdock_can_assign`, `permdock_can_assign_for`, `permdock_holders_<scope>`, `permdock_transfer_only_<scope>` | The ownership objects `permdock rls generate` adds when a role declares a rule: `permdock_can_assign(p_role, p_scope_id)` for the application's policies on its membership tables (a null `p_scope_id` for a global role), `permdock_can_assign_for(p_user, p_role, p_scope_id)` for trusted SQL acting for a stored user in `database` mode, the deferred holder-count trigger (`min`, `max`) and the transfer-only statement triggers on each scope's membership table ([ownership](/docs/concepts/ownership)) | A trigger that writes; a check that trusts the caller's role claim over the membership table |
| `<table>_visible`, `<table>_visible_fields`, `permdock_key`, `--fields views`, `--revoke-columns`, `rls.fields`, `rls.revokeColumns` | The field views `permdock rls generate --fields views` emits: a `security_invoker` view per table with field-limited read grants whose restricted columns are `case when <permitted> then col end`, and with `--revoke-columns` the owner-rights companion it joins on `permdock_key` while the table keeps only unrestricted columns readable; `rls import` reads them back as the `fieldViews` export, `rlsParity({ fieldViews })` and `rls verify` compare them with `pick`, doctor PD030 flags columns still readable | Postgres column privileges per app role, a permission per field, or a view that reads past RLS |
| `secretKeys`, `SupabaseSecretKey` | The `permdock/supabase/middleware` option mapping a named Supabase secret key (`authKeyName` from `withSupabase`) to a `service` principal with `{ id, tenant, roles, permissions }`, as a `service` credential | A list of accepted keys; `withSupabase`'s `auth: 'secret:<name>'` decides which keys verify |
| `withPermDock` | The `@supabase/middleware` entry `permdock/supabase/middleware`'s `createPermDock` returns: requires `jwtClaims` upstream, contributes `ctx.permdock`; `withPermDock({ protect, data })` short-circuits with Problem Details | A PermDock naming pattern; `with*` is the pipeline's convention and appears elsewhere only as `withSubject` and `withOtel` |
| `createJwtSubjectResolver` | The cached, reusable form of `subjectFromJwt`, one per issuer ([JWT adapter](/docs/adapters/jwt)) | A second `createPermDock` |
| `createClerkSubjectResolver` | The `subjectFromClerk` resolver that caches each user's `memberships: 'all'` list for `cache.ttl` ([Clerk adapter](/docs/adapters/clerk)) | A token or session cache |
| `verifyDpopProof` | The exported DPoP check `permdock/jwt` also runs when `sender: 'dpop'`; `replay` (a `ReplayStore`, `memoryReplayStore()` from `permdock/jwt`) refuses a reused proof `jti` | A second verifier; it only checks the proof |
| `TokenVerifier`, `TokenSigner` | The two JOSE interfaces core declares as types ([extension interfaces](/docs/concepts/extension-interfaces)) | Anything that throws; anything that decides |
| `joseTokenVerifier`, `joseTokenSigner` | The `permdock/jwt` implementations of the two interfaces over the optional `jose` peer | Exports of core |
| `verifier`, `signer` | The option names for a `TokenVerifier` (`subjectFromJwt`, `permdock/ssf`) and a `TokenSigner` (`permdock.snapshot`, `approvalsHandler`, `permdock/cloud`) | `jwt`, `jose`, `crypto` as option names |
| `discovery`, `accept` | The `permdock/jwt` options for OpenID Connect Discovery (an issuer URL that supplies `jwks_uri` and `issuer`) and for which token kind a resolver accepts (`'access-token'` default, `'id-token'`) | Guessing the token kind from its claims |
| `subjectFromIntrospection` | An RFC 7662 or RFC 9767 introspection response in, `Subject` out | An HTTP client; the call is yours |
| `<provider>RoleSource` | A provider's `RoleSource` implementation (`betterAuthRoleSource`) | A subject mapper |
| `authorizationProvider` | `permdock/better-supabase`: better-supabase's `authorization` config value (`AuthorizationProvider`, `apiVersion: 1`), built from the manifest and catalog; `approver` sets `canApprove` | PermDock's own decision path |
| `bucketPolicy`, `topicPolicy` | `permdock/better-supabase`: the `access` policy of `defineBucket` and `defineTopic`, mapping operations to `Permission` references | `rls.storage` and `rls.realtime`, which `permdock rls generate` writes |
| `apiKeyClaimOptions` | `permdock/better-supabase`: the `claim` and `tenantClaim` options of better-supabase's `apiKeyClaims()` and `apiKeyResolver()`, from `rls.apiKeys` | Verifying a key |
| `subjectFromBetterSupabase` | `permdock/better-supabase`: a `Subject` from better-supabase's `AuthSession`, with `apiKeys` for `apiKey` sessions and `plans` for the `features` claim | Verifying the session; better-supabase did that |
| `toolPolicy` | `permdock/better-supabase`: the `authorize` and `visible` hooks of better-supabase's `createMcp`, for tools whose `meta` is a `Permission` | Tools on the official MCP SDK (`permdock/mcp`) |
| `credentialGuard` | `permdock/better-supabase`: a better-supabase `CredentialProvider` whose token uses PermDock decides first | Storing or refreshing tokens |
| `describe` | `describe(decision)` returning `{ kind, title, detail, alternatives }` for tooltips and Problem Details | Logging; it is pure |
| `Snapshot` | The JSON type of `permdock.snapshot()`; format major is the `v` field | A versioned type name (`SnapshotV2`); majors live in `v` |
| `parseSnapshot` | Read snapshot JSON; rejects unknown majors and forbidden keys (`__proto__`, `constructor`, `prototype`) | Evaluating; it is a reader, not `createPermDock` |
| `fromSnapshot` | Build a client `PermDock` from a `Snapshot` so portable grants evaluate locally | A second policy; closures stay on the server |
| `snapshotFor` | The synchronous, deterministic snapshot builder for app-owned `'use cache: private'` functions ([snapshots](/docs/concepts/snapshots)) | A function that adds a cache directive, reads headers or awaits a source |
| `snapshotPromise` | `PermDockProvider` prop on `permdock/react`: an unawaited `Promise<Snapshot>` from a Server Component; hooks answer `pending` until it resolves, or suspend through `use()` with `suspend` | Awaiting the snapshot in a layout |
| `cacheLifeFor` | `permdock/next`: `{ stale }` for `cacheLife()` from a snapshot's `expiresAt` and `issuedAt`, clamped to 30..300 seconds | A cache directive; the app calls `cacheLife` |
| `mayAccess` | `mayAccess(policy, user, permission, { tenant })`: optimistic check for a proxy; `false` only when the declared roles provably lack the permission | A decision; the page still calls `can` / `decide` |
| `mayUse` | `mayUse(permdock, permission)`: whether some grant in the instance's snapshot could match for its subject, active tenant and delegation; the listing hint `permdock/mcp`, `permdock/a2a` and hosts with their own MCP server (better-supabase `createMcp`) use to filter tools | A decision; the call still runs `can` / `decide` |
| `approvalHeaders` | `{ 'PermDock-Approval': token }` for a retried mutation | Minting a token; it only wraps the one `decide` returned |
| `ApprovalStore`, `memoryApprovalStore`, `approvalsHandler`, `cancelApprovals` | The pluggable store behind `approval-required`, its in-process default, the Fetch routes for approvers, and the helper that rejects pending requests for a session (`permdock/approvals`) | Deciding; a store never influences `decide` |
| `consume`, `consumedAt`, `consumeApproval`, `resumeDecision`, `storedApprovalToken` | The atomic single-use step on `ApprovalStore`, the field it sets, and the helpers that apply a resume token, or find the stored one for a recomputed decision | `used`, `redeemed`, a `consumed` status |
| `ApprovalError`, `isApprovalError`, `assertApprover` | The error (with a closed `code` list), its guard (matches by `name` and `code`, so a duplicated module copy still maps) and the approver check (`by`, escalation once open, no repeat) a custom `ApprovalStore` applies in `resolve` | Plain `Error` from a store, which `approvalsHandler` maps to `500` |
| `permdockApproval` | The context key the `ai-sdk` and `openai` adapters read an explicit resume token from; every agent adapter also recomputes the token and finds the record in its `store` without it | `approval`, `token` (collide with application context) |
| `APPROVAL_META_KEY` | `'dev.permdock/approval'`, the MCP request `_meta` key `permdock/mcp` reads a resume token from | A token in tool arguments (model-controlled) |
| `approval: 'human'` or `{ by, distinct, staleOn, quorum, ttl, escalation }` | Grant option that yields `approval-required`; `by` is a grantee selector; `distinct` (default `true`) refuses the principal as approver, and `distinct: false` is the explicit opt-out doctor PD024 reports; `staleOn: 'resource-change'` binds the approval to the row's `version`; `quorum` (default 1) is the number of distinct approvers; `ttl` is a duration that caps how long the request stays open; `escalation: { after, to }` widens the approvers after a duration | A fluent approver builder; `allowSelf`; `approvers` on the grant (that is the request's copy); `threshold`, `minApprovals` |
| `holder`, `anyOf`, `allOf`, `PermissionApprover`, `AnyOfApprover`, `approverPermissions`, `permdockFor`, `verdict.permissions` | Approvers who hold a permission in the request tenant (custom roles included), an any-of group, an explicit all-of list, and the verdict facts that `holder()` is checked through | `hasPermission()`, `oneOf`, `or()` |
| `mode`, `stages`, `user`, `UserApprover`, `Approver` | The approval mode (`'any'`, `'all'`, `'sequential'`), the ordered approver sets it takes, the approver for one named person, and the union of approver kinds | A workflow engine; `steps`, `chain`, `levels`, `person` |
| `approverRelations`, `approverRelationKey`, `relations` (verdict) | The handler step that reads which relation approvers an approver satisfies, the key each one is stored under, and the verdict field that carries those keys to the store | Facts from a request body |
| `ApprovalEscalation`, `ApprovalSignature`, `approvals` | The normalised `escalation` on a grant, one recorded approval `{ by, at }`, and the request field that lists them oldest first | `signatures`, `votes`; a count without who gave it |
| `applyApprovalVerdict`, `approvalQuorum`, `escalationOpenAt` | The helpers a custom `ApprovalStore` uses in `resolve`: the next request after a verdict (quorum and escalation applied), the approvers a request needs, and the instant its escalation opens | A store re-implementing the quorum rules |
| `vouchApproval`, `vouched` | A verdict the application's own approval rules decided, recorded with the rule's name: it resolves the request at once without the `approvers` eligibility checks and keeps the separation checks | A verdict from a request body; a flag that also skips the actor and requester checks |
| `approver-repeated` | The `ApprovalError` code for a principal who already approved the request; `approvalsHandler` maps it to `409` | `duplicate-approval`; counting the second approval |
| `version` | The `resource()` option naming the row field (`updatedAt`, a revision) that a `staleOn: 'resource-change'` approval binds to; also on catalog resources | A schema version; the snapshot or catalog format major |
| `stale-approval` | The denial reason for a resume token that approved an earlier `version` of the row | `approval` with a `detail` |
| `exclusiveWith` | Role option listing roles that must not be held together; doctor PD018, `separationConflicts` and `decideRoleChange` (`conflicting-role`) | An evaluation deny |
| `DecisionSink`, `memorySink` | The pluggable destination for `on('decision')` events and its in-process default; `memorySink({ signer })` signs each write | Blocking a decision; sinks are fire-and-forget |
| `signDecisionBatch` | Sign a batch of sink events as a `permdock-decisions+jwt` JWS | Feeding a signed batch back into `decide`; evidence only |
| `Capability`, `CapabilityInput`, `CapabilityRedeemer`, `parseCapability` | The v1 share-link object `{ v, id, holder, on, roles, permissions?, redeemer?, once?, expiresAt }`, the `signCapability` input (permission and resource references, not keys), who may redeem it (`'anyone'`, `'signed-in'`, `{ user }`, `{ scope, id }`), and the strict own-property parser ([link capabilities](/docs/concepts/capabilities)) | `ShareLink`, `LinkToken`, `audience` (that is the JWT `aud`) |
| `signCapability`, `capabilitySubject` | Sign a capability as a `permdock-capability+jwt`; build the `link` subject a verified capability acts as | Issuing without a guard; a link that holds a scope or global role |
| `subjectFromCiOidc`, `CiOidcSubjectOptions`, `CiOidcPrincipal`, `CiOidcProvider` | `permdock/jwt`: verify a CI job's OIDC token (`provider: 'github' \| 'gitlab' \| 'buildkite'`, required `audience`, optional `issuer` and `schema`) and return a `workload` principal with `repository`, `ref` and `environment`; `on('auth')` source `ci-oidc` | A user principal from a CI token; a long-lived CI secret |
| `subjectFromCapability`, `CapabilitySubjectOptions`, `CapabilityFailureCause` | `permdock/jwt`: verify a capability and return the link subject, never throwing; options `issuer`, `audience`, `linkPolicy`, `revoked`, `replay`, `viewer`; causes `redeemer-mismatch`, `link-policy`, `capability-revoked`, `capability-replayed` next to the token causes | Accepting a capability in `subjectFromJwt`; a capability as an access token |
| `LinkPrincipal`, `kind: 'link'`, `via: 'link'` | The principal a capability resolves to (its `id` is the link id, with the verified `capability` attached) and the `via` of its one resource membership | A user id; a principal a model or request body can name |
| `LinkPolicy`, `LinkPolicyViolation`, `linkPolicyViolation`, `linkPolicy` | A scope instance's rules for links on its resources (`maxLifetime`, `redeemers`, `once`), the broken rule (`lifetime`, `redeemer`, `once`), the check, and the option on `subjectFromCapability` and `signCapability` that applies them | A rule that widens; a policy read from the token |
| `holder` | `'link'`, or the reserved `'key'` for a future stateless key the resolver refuses; API keys are opaque `pdk_` keys, not capabilities | A third holder kind |
| `Credential`, `CredentialKind`, `CredentialPermission`, `parseCredential` | The v1 record an API key stands for `{ v, id, kind, principal, tenant?, roles?, permissions, createdBy, createdAt, expiresAt?, name? }`, `'user'` or `'service'`, one `{ permission, ids? }` entry, and the strict own-property parser ([API keys](/docs/concepts/credentials)) | `ApiKey` for the record; the secret or its hash in the record |
| `CredentialPrincipal`, `via: 'credential'` | A principal resolved from a key, with the verified `credential` attached; the `via` of a service key's one tenant membership | A principal a request body can name |
| `credentialSubject`, `credentialDelegation` | The subject a verified credential acts as (a user key's owner narrowed by delegation, a service key's service principal) and the delegation alone | Copying the owner's rights into the key |
| `decideCredential`, `CredentialRequest`, `CredentialPermissionInput`, `CredentialDecision`, `DecideCredentialOptions` | Decide whether the subject may create a key: `granted` with the `Credential` to store, `approval-required` with a bound `token` (resume with `approved`), or `denied`; options `settings`, `approved`, `now` | Storing the key; a key that mints keys |
| `CredentialPolicy`, `CredentialPolicyViolation`, `credentialPolicyViolation` | A tenant's API-key rules (`maxTtl`, `kinds`, `approval`, `allowNoExpiry`), the broken rule (`kind`, `no-expiry`, `ttl`) and the check | A rule that widens |
| `SettingsSource`, `TenantSettings`, `memorySettings`, `settings` | Per-tenant settings (`settingsFor(tenant)` returns `{ credentials? }`), its in-process default and the option on `decideCredential` and `subjectFromApiKey` | Settings that grant; a module-level settings object |
| `CredentialVerifier`, `apiKeyVerifier`, `memoryCredentials`, `verifier` | `{ verify(key) → Credential \| null }`, the verifier over an application table (`find(id)` returns `{ credential, hash }`), the optional `touch(id, at)` for `lastUsedAt`, the in-process store (`issue`, `rotate`, `revoke`, `list`, `lastUsedAt`) and the option naming a verifier | Verifying keys in core; scanning every row |
| `subjectFromApiKey`, `ApiKeySubjectOptions`, `ApiKeyFailureCause` | `permdock/server`: a `SubjectResolver` for `pdk_` keys, never throwing; options `verifier`, `permissions`, `owner`, `revoked`, `settings`, `sink`, `sample`; causes `malformed`, `unknown-credential`, `invalid-claims`, `expired`, `credential-policy`, `credential-revoked`, `owner-unavailable` | An API key in `subjectFromJwt`; a key as an actor |
| `generateApiKey`, `hashApiKey`, `parseApiKey`, `StoredCredential` | A fresh `pdk_<id>_<secret><checksum>`, its base64url SHA-256, the `{ id, secret }` split, and the `{ credential, hash }` row a verifier reads | Storing the key itself |
| `CredentialEvent`, `credentialEvent`, `dev.permdock.credential` | A `credential` sink event (`created`, `used`, `rotated`, `revoked`; `used` carries `sample`), its builder and CloudEvents type | A per-operation CloudEvents type |
| `exchangeCapability`, `ExchangeCapabilityOptions` | `permdock/supabase`: a verified link subject in, a short-lived `role: 'anon'` Supabase access token with a `capability` claim out | `service_role`; a `sub` claim; a security-definer RPC per resource |
| `rls.anonymousSignIns` | `'deny'` keeps a Supabase token with `is_anonymous: true` out of every RLS branch but the `anyone()` grants' | A boolean; a separate role for anonymous sign-ins |
| `rls.anonExecute` | `true` grants `anon` usage on the helper schema and `execute` on the helpers, for hand-written policies that apply to `public` or `anon` | A grant to `service_role`; a way to let `anon` read rows |
| `rls.tenants` | `'active'` (default) narrows the generated helpers and `memberOf` checks to the tenant claim when the token carries one; `'all'` admits every tenant the subject is a member of, for apps whose tenant comes from the URL | A boolean such as `narrowTenant`; an organisation-slug header |
| `permdock_trusted_replace_custom_role_grants`, `permdock_trusted_rename_custom_role_grants`, `permdock_trusted_delete_custom_role_grants` | The generated custom-role writes for a trusted caller (a migration, a job, a backend role granted `execute`): the definition checks without the caller checks; no client role may execute them | `service_role` grants in generated SQL; an `admin` or `bypass` flag on the member functions |
| `permdock_replace_global_roles` | The generated `rls.roles` write that sets one user's global roles in one call, as the invoker so the assignment trigger still judges each row; `execute` for `authenticated` only | A `security definer` variant that skips the ceiling; `replace_roles` without the scope word |
| `rls.customRoleWrites.requires`, `manage-roles` | The permissions (or `'manageRoles'`) a caller must hold before the generated custom-role writes run the hand-out check, and the `hint` when it holds none | A role name list; a check that only runs on save |
| `rls.customRoleWrites.roles`, `permdock_cascade_custom_role` | The application's table of custom roles (`table`, `key`, `tenant`, `scope`, `id`, `skip`) and the generated trigger function that carries a role's grants through renames, moves and deletes of its row | A second store of custom role names; a trigger that writes without the write functions' checks for a signed-in caller |
| `rls.customRoles.from`, `--backfill-out`, `-- permdock:backfill v1` | The application's existing custom-role tables (`permissions`, `includes`) and the flag that writes the idempotent data migration copying them through the trusted replace function, with its marker line | `import`, `adopt` or `sync` for a one-off copy; a copy that overwrites a role edited since |
| `rls.triggers`, `rls.audit` | Triggers the application owns on generated tables (`name`, `when`, `events`, `level`, `function`, `args`), and one call per generated table to an audit module that registers it by `regclass` | A `permdock_` trigger name for an app trigger; a hand-written migration that has to run after the generated file |
| `rls.migrate.tables`, `refresh`, `--retire-out`, `-- permdock:retire v1` | The materialised permission tables `rls migrate` retires, the functions that refresh one, the flag that writes the drop migration once nothing reads them, and its marker line | `cascade`; `drop`, `remove` or `cleanup` for the flag |
| `rls.assignments`, `rls.assignments.ownRole`, `permdock_assignment_<table>`, `permdock_can_assign_custom_role`, `permdock_can_assign_custom_role_for` | Assignment triggers on the membership tables, the global-roles table and listed tables such as invitations: a client role may write only the roles it may assign, by `assigns` or, for a custom role, by what it may hand out, and with `ownRole: 'refuse'` none on its own rows; other writers are trusted, and the `_for` form re-checks a stored user | A setting a client could flip to bypass the check; a trigger that trusts the role claim |
| `rls.ownershipTriggers` | `false`, or `{ <scope>: false }`, leaves out the holder-count and transfer-only triggers of `min`, `max` and `transferOnly` | A way to turn the rules off in `decideRoleChange` |
| `permdock_can_assign_any`, `permdock_can_assign_any_for` | One assignment check for a declared or a custom role, `(p_role, p_tenant, p_scope, p_scope_id)`, written when a role declares `assigns`; `p_scope` `'global'` assigns a platform role, and the `_for` form takes the stored user first | A caller-side branch on whether a role is declared; a check that trusts the role claim |
| `rls.readOnlyActors` | Restrictive policies, `{table}_{op}_read_only_actors`, that refuse writes from a token whose `act.kind` is a listed actor kind (`true`: `support` and `impersonation`) unless `act.read_only` is `false` | A permissive policy that widens access; a session flag a client could set |
| `rls.realtime.topics`, `permdock_realtime_<pattern>_<op>` | Private Realtime channel policies on `realtime.messages`: a `:`-separated topic pattern with one `{<scope>}` segment, `read` (`select`) and `write` (`insert`) permission references | A policy keyed on the row's `topic` column; a public channel |
| `rls.storage.buckets`, `permdock_storage_<bucket>_<op>` | Storage policies on `storage.objects` for one bucket: `scope`, `folder` (1-based, default 1), `read`, `write` (`insert` and `update`) and `delete` permission references | Owner-only policies on `owner_id`; a bucket-wide grant |
| `rls.apiKeys`, `permdock_api_key_allows(p_grant)` | An API key passed in a claim (`api_key` by default) caps every allow at its `scopes` (permission keys), and a key with a `tenant` and no subject is a service principal holding `roles` (or `serviceRoles`) in that tenant | A wildcard scope; a key that widens its owner's rights; a key set by a client |
| `permdock_user_id()` | The Supabase helper every generated helper, policy, field view and trigger reads the subject through: what `auth.uid()` returns, and null for an empty `sub` | `auth.uid()` in a policy a token with an empty `sub` reaches; a user id the caller passes |
| `permdock_session_live()` | The Supabase helper a `{ subject: { session: { live: true } } }` condition compiles to: the token's `session_id` is in `auth.sessions` for the caller and not past `not_after` | `auth.uid()` alone, which a revoked session's token still passes |
| `permdock_capability_ids`, `--capabilities`, `rls.capabilities` | The RLS helper that reads the `capability` claim, and the flag and config key that emit it with one `anon` policy per resource-scoped grant | A policy that trusts a capability the server did not exchange |
| `store`, `sink` | Options for an `ApprovalStore` and a `DecisionSink`; a `SnapshotSource` is the `source` option of `permdock/react-native` | Anything else; `store` is never a database handle |
| `policies` | The option every adapter's `createPermDock` accepts for a `PolicySource`; read once per instance | A module-level policy the source swaps; a per-check fetch |
| `hostable` | `definePolicy` option listing the permission subtrees or leaves hosted grants may touch; default none | `hosted`, `editable`, `remote`; a grant option |
| `PolicySource`, `memoryPolicySource`, `testPolicySource` | The channel for hosted grants (`current()`, `refresh()`), its in-process default and its `permdock/testing` runner | A decision path; `current()` is synchronous and never fetches |
| `PolicyDocument`, `HostedGrant`, `parsePolicyDocument`, `mergeHostedGrants` | The `v: 1` hosted-grant document, one grant in it, its reader (rejects unknown majors and forbidden keys) and the pure merge `createPermDock` runs | `PolicyDocumentV2`; majors live in `v` |
| `HostedGrantRef`, `matched.hosted` | `{ document, grant }` on a grant and a Decision a hosted grant matched: the document fingerprint and the grant id | A role name; hosted grants still name declared roles |
| `hosted-grant-dropped`, `HostedGrantDropped` | The `on('error')` value for a hosted grant that failed a merge rule; `reason` is `not-hostable`, `unknown-permission`, `unknown-grantee`, `non-portable`, `weaker-approval` or `invalid` | A denial reason; a dropped grant never reaches `Decision` |
| `CLOUD_EVENT_TYPES`, `CloudEventType`, `CatalogEventData` | The closed CloudEvents `type` list (`dev.permdock.decision`, `.approval`, `.directory`, `.membership`, `.catalog`) and the `catalog` payload `{ kind, fingerprint, previous?, findings? }` | A new `type` without a wire-format change |
| `CatalogFinding`, `CatalogFindingCode` | One drift finding `{ code, permission, grant? }`; `code` is `permission-removed`, `not-hostable`, `grantee-removed` or `approval-tightened` | Free-text finding lines |
| `coveredByDelegation` | `permdock`: the delegation coverage check (`scopes`, RAR `authorizationDetails`, GNAP `access`) `decide` uses; returns `undefined`, `no-delegation` or `not-delegated` | `isDelegated`, `checkScopes`, a boolean that hides the reason |
| `delegations`, `DelegationInput`, `PolicyDelegation`, `DelegationTarget` | The `definePolicy` option for standing delegations `{ from, to, permissions, validFrom?, validUntil? }`, its input type, the normalised entry `{ from, to: { kind, id? }, permissions: string[], validity? }` on `policy.delegations`, and the actor it is for | `agents`, `grantsToActors`; a grant with an actor grantee (that gives the actor access of its own) |
| `delegatedPermissions`, `delegated` | `permdock`: the ceiling of permission keys the policy's delegations give the subject's actor right now (`undefined` when none applies), and the snapshot field carrying it sorted | `delegationScopes` (these are permission keys, not OAuth scopes); a field on the subject |
| `delegation-removed`, `delegation-narrowed` | `permdock diff` breaking kinds for a policy delegation that disappeared or lost permission keys or validity | `delegation-changed` for a narrowing |
| `catalogFingerprint` | `permdock`: the catalog v1 fingerprint (base64url SHA-256 of canonical JSON without `generatedAt`, `generator`, `fingerprint` and `usages`) the CLI writes and the Cloud recomputes | A hash of the pretty-printed file; a CLI-only helper |
| `WireDenial`, `WireDecision` | `permdock`: a denial as it leaves the process (`{ role, reason, to?, detail? }`, `detail` a JSON value, never a closure cause or validation error) in decision events, Problem Details, MCP and WebMCP refusals and the decision endpoint; `WireDecision` is a `Decision` with those denials | `SerializedDenial`, `PublicDenial`; sending `Denial` as is |
| `toCsvRow`, `CSV_COLUMNS` | `permdock`: one RFC 4180 CSV row for a decision event and the pinned header (`time, principal, actor, tenant, permission, outcome, matched.role, via, denials.reason, token`) | A configurable column list; a Cloud-only exporter |
| `toOcsf`, `OCSF_VERSION`, `OcsfAuthorizeSession` | `permdock`: the pure projection of a decision event onto OCSF Authorize Session, pinned to one OCSF version | A second, Cloud-only projection |
| `verifyWebhook`, `parseCloudEvent`, `PermDockCloudEvent` | `permdock/cloud`: verify a signed Cloud webhook delivery (never throws, no unsigned mode) and validate one CloudEvent | A shared-secret or unsigned webhook mode |
| `PermDockEnv` | `permdock/hono`: the `Env` type the `permdock()` and `protect()` middleware declare, so `c.var.permdock` and `c.var.permdockData` are typed | A `declare module` augmentation of `ContextVariableMap` |
| `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, environment, verifier })` from `permdock/cloud`, returning `approvals`, `sink`, `snapshots`, `policies`, the environment URL as `issuer` and its `jwks` URL | A factory for a `PermDock`; there is no `decide` on it; an app `audience` option |
| `cloudEndpoints` | `cloudEndpoints({ url?, environment? })` from `permdock/cloud`: the environment URL (`issuer`) and JWK Set URL (`jwks`) with `cloud()`'s fallbacks, for building a verifier before `cloud()` | A network call; discovery |
| `scimHandler` | The RFC 7644 Fetch handler from `permdock/scim` writing into a `DirectoryStore` ([SCIM adapter](/docs/adapters/scim)) | A `PermDock`; it decides nothing and reads no tenant from a body |
| `DirectoryStore`, `memoryDirectoryStore` | The repository interface SCIM provisioning writes to (users, groups, `groupsFor`), every method tenant-first, and its in-process default | A `MembershipSource`; the store is the write side, the source below is the read side |
| `directoryMembershipSource` | `directoryMembershipSource(store)`: the `MembershipSource` that turns synced groups into tenant memberships `{ tenant, roles, via: 'group:<id>' }` and yields nothing for an inactive user | Reading display names; group ids only |
| `ReplayStore`, `memoryReplayStore` | The pluggable `jti` replay store for `permdock/ssf` and its in-process default | A decision input; SETs and `logout_token`s never reach `decide` |
| `connection`, `Connection`, `ConnectionOptions` | The kernel and HTTP adapter method that opens a long-lived connection for a stream or socket, and its frozen result (`permdock`, `signal`, `check`, `filter`, `close`) | A mutable instance; `stream`, `socket` or `subscribe` as the kernel method name |
| `RevocationFeed`, `RevocationEvent`, `memoryRevocationFeed` | The interface that tells open connections a subject changed (`subscribe`, `revoke`; events `session-revoked` and `changed`) and its in-process default | A decision input; a feed ends or revalidates, never grants |
| `revocations` | The option carrying a `RevocationFeed` on the kernel, the HTTP adapters, `permdock/ssf` and `permdock/scim` | `revocationFeed`, `feed` |
| `PermDockRevokedError`, `RevokedCode` | The `reason` of an aborted connection signal; `code` is `session-revoked`, `expired`, `denied` or `subject-changed` | A denial reason; a check after the abort is `no-grant` with `detail: 'connection-revoked'` |
| `socket`, `sse`, `SseOptions` | `permdock/hono` helpers: `socket(conn, events)` wraps `upgradeWebSocket` events, `sse(conn, stream, source, options)` writes an async iterable to `streamSSE` | A `with*` or `Dock` name; helpers for other adapters |
| `ElysiaSocket`, `NestSocket` | The structural socket types `connection` accepts in `permdock/elysia` and `permdock/nest` | An import of the framework's socket class |
| `ApprovalListQuery`, `ApprovalPage` | The `ApprovalStore.list` argument (`ApprovalListFilter` plus `limit` and `cursor`) and its result (`items`, `next`) | An offset, or a page number |
| `ApprovalHint` | `{ at?, hint? }`: the `approval` option on the server kernel, HTTP adapters and `permdock/terminal`, emitted as the `approval` member of `approval-required` Problem Details | A terminal-only field, a URL built from the token by PermDock |
| `KeyringEntry`, `storage.keyring` | The `Entry` shape from `@napi-rs/keyring` and the `permdock/terminal` storage option that takes it | A bundled keychain binding, `keytar` |
| `remotePdp` | `permdock/pdp` helper that implements `DecisionProvider` over an AuthZEN evaluation URL | A local `decide`; unknown or unreachable remote answers deny |
| `openfga`, `spicedb`, `RelationMap` | `permdock/pdp` presets that implement `DecisionProvider` over OpenFGA `check` / `list-objects` and the SpiceDB HTTP gateway `CheckPermission` / `LookupResources`, and their one-callback-per-permission tuple map | A tuple store, a model DSL or a per-vendor entry |
| `DecisionProvider.permitted` | The optional listing member: the ids of one resource type a subject may act on, used by the PDP instance's `filter` and `where` | A grant; `null` denies every row |
| `PermDock-Approval` | The HTTP request header carrying an approval `token` on a retried request | Anything else on the wire |
| `membershipEvent` | Pure builder for a `membership` sink event | A `Decision`; it is audit only |

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](/docs/concepts/authentication).

## Reserved words [#reserved-words]

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

| Word | Why it is reserved |
| --- | --- |
| `dock` | It reads oddly next to `PermDock`; only `permdock` is used for the instance. |
| `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](https://github.com/stalniy/casl/blob/master/docs-src/src/content/pages/cookbook/less-confusing-can-api/en.md)). 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. |
| `Can`, `Guard`, `Access` | Component names used by CASL and Kilpi; the PermDock gate is `<Protected>`. The only other component is `<PermissionBoundary>` in `permdock/next/client`, `permdock/vue`, `permdock/svelte` and `permdock/solid`: an error boundary for thrown PermDock errors, never a gate. |
| `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`, `useAssignablePermissions`). |
| `who` | Reserved for a directory-backed "who may X this row" verb; not exported. |
| `SnapshotV2` | The format major lives in `v`; the type is `Snapshot`. |
| `scope(...)` as a derived instance | Derived instances are `tenant(id)` and `team(id)`, one method per scope kind. |
| `permdock.with(...)` | `with*` names the Supabase middleware pipeline steps, `withSubject` and `withOtel`; an instance with other sources is `permdock.derive({ customRoles, approvalPolicies, relations })`. |
| `@permdock/cli`, `@permdock/testing`, any `@permdock/*` package | `permdock` is the only published name, owned by the `scaledockhq` npm organisation. The CLI is its `bin` plus `permdock/cli`, `permdock/unplugin` and `permdock/next/plugin`; the runners are `permdock/testing`. One package keeps core, adapters, CLI and runners on the same version. |
| `permdock/cloud-auth` | There is no such entry: PermDock Cloud identity is `subjectFromJwt({ discovery })` against the Cloud issuer. |

## What each adapter's createPermDock returns [#what-each-adapters-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, getSnapshot, requireAccess, PermDockProvider, permdockHandler }`, plus the standalone `cacheLifeFor(snapshot, { min, max, now })`, `snapshotTag` and `snapshotHeaders` shared with `permdock/server` | Options: `subject`, `tenant`, `memberships`, `customRoles`, `store`, `sink`, `otel`, `wrap`; never a cache directive (`getSnapshot` sets `cacheLife` and `cacheTag` inside the app's `'use cache: private'` function, and its `tags` add to `snapshotTag`); `requireAccess`, not `requirePermission` or `authorize`, because it maps a decision to the `forbidden()` / `unauthorized()` interrupts |
| `permdock/next/client` | `PermissionBoundary` (props `denied`, `approval`: a node or a `PermissionBoundaryFallback` function of the state) and `usePermissionBoundary()` returning `{ outcome, permission, token?, retry }` | A `'use client'` entry built on `catchError` from `next/error`; the boundary never decides, it renders a fallback for an error a check already threw |
| `permdock/server` | `{ permdock, getSnapshot, protect, problem, openapi, permdockHandler }`, plus the standalone `problemFromError(error)`, `snapshotHeaders(snapshot, { tags })`, `cacheLifeFor` and `snapshotTag` | The Fetch kernel every HTTP adapter wraps. Options: `subject`, `tenant`, `memberships`, `customRoles`, `store`, `sink`, `limits`, `pdp`, `otel`, `webBotAuth`, `actor`, and `InstanceOptions` (`approvalPolicies`, `relations`, `entitlements`, `policies`) |
| `permdock/hono` | `{ permdock, protect, permdockHandler }` | `permdock()` is middleware; `protect(permission, load?, options?)` guards a route |
| `permdock/express`, `permdock/fastify`, `permdock/elysia`, `permdock/node` | `{ permdock, protect, permdockHandler }`, plus `errorHandler` and `withPermDock(fn)` (Express) and `send` (Node) | Same shape as Hono over the shared kernel |
| `permdock/nest` | `{ PermDockModule, PermDockGuard, Protect, InjectPermDock, permdockHandler }`, plus `decorateMethod` | `PermDockModule.forRoot({ guard: 'global' })` registers `PermDockGuard` as `APP_GUARD`; `permdockHandler({ path })` sets the controller path |
| `permdock/terminal` | `{ permdock, protect, filterCommands, format, exitCode }`; `EX_OK`, `EX_USAGE`, `EX_TEMPFAIL`, `EX_NOPERM`, `EX_CONFIG`; options `yes` and `dryRun` (`--yes` / `-y`, `--dry-run`), `interactive: { confirm, typed }` | For your own CLI; `protect` wraps a command action, `filterCommands` hides or annotates denied commands. Not the `permdock` binary or `permdock/cli` |
| `permdock/trpc` | `{ permdock, protect, permdockHandler }` | `protect` is procedure middleware; denials throw `TRPCError` |
| `permdock/orpc` | `{ permdock, protect, permdockHandler }`, `permissionOf` | `protect` is procedure middleware; denials throw `ORPCError` through the contract's declared constructor when it has one |
| `permdock/mcp` | `{ protectServer }` | `protectServer(server).registerTool(name, { permission, data?, longRunning?, ... }, handler)`, plus `registerResource` and `registerPrompt`; `protectServer(server, { enforce: 'procedure', permissionFor })` lists by permission and leaves the decision to the procedure; options: `subject`, `requireAuthInfo`, `resource` (RFC 8707), `store`, `approval` (`{ at, hint }`), `stepUp` (`{ at }`), `requestState` (an SDK `createRequestStateCodec`); also exports `subjectFromMcp`, `APPROVAL_META_KEY`, `McpPrincipal` |
| `permdock/ai-sdk` | `{ toolApproval, capabilityMiddleware, needsApproval }` | Options: `subject`, `actor`, `tools` |
| `permdock/claude-agent` | `{ canUseTool, permissionRequestHook, permdock }` | Options: `subject`, `tools`, `store`, `mcpSources` (trusted MCP server sources, default `['sdk']`) |
| `permdock/eve` | `{ approval, approvalFor, permdock }` | `approval` is Eve's `{ request, response }` pair for `defineTool` (`EveApprovalPair`, contexts `EveApprovalContext` and `EveResponseContext`, principals `EvePrincipal`); options: `tools`, `store`, `approvers` (`EveApprovers`); also exports `subjectFromSession`, `actorFromSession`, `rolesOf` |
| `permdock/openai` | `{ needsApproval, guardTools, resolveInterruptions, permdock }` | Options: `subject`, `actor`, `tools`, `store` |
| `permdock/authzen` | `{ permdockHandler }` | Serves evaluation, evaluations, search and `.well-known` |
| `permdock/ssf` | `{ receiver }` | RFC 8935 push and RFC 8936 poll receiver for CAEP events; `receiver.logout` for OIDC Back-Channel Logout |
| `permdock/pdp` | `{ createPermDock }` wrapping a local policy plus optional `remotePdp` | Opt-in AuthZEN client; fail-closed on unknown remote decisions |
| `permdock/convex` | `{ withPermDock, snapshotQuery }` | Convex query/mutation wrappers; no Convex SDK peer (structurally typed); options `subject`, `tenant`, `actor` |
| `permdock/scim` | no `createPermDock`; exports `scimHandler`, `memoryDirectoryStore`, `directoryMembershipSource` and the `DirectoryStore` type | The handler is a Fetch handler you mount; the source is a value you pass as `memberships` to a factory |
| `permdock/a2a` | `{ agentCard, extendedAgentCard, protectSkill, sign }` | Options: `skills` |
| `permdock/otel` | no factory; `instrument` / `withOtel` | Subscribes to `on('decision')`; optional `@opentelemetry/api` |

These entries deliberately do not export `createPermDock`:

* `permdock/react` and `permdock/react-native` export `PermDockProvider`, `usePermDock`, `usePermission`, `usePermissions`, `useFilter`, `useTenant`, `useMemberships`, `useRoles`, `useAssignableRoles`, `useAssignablePermissions`, `useApproval`, `useSubject`, `useDescribe` and `<Protected>` directly. `permdock/react-native` adds `subjectId` (required, `null` when signed out), the storage wrappers `memoryStorage`, `secureStoreStorage` and `mmkvStorage`, the subscriptions `appStateForeground` and `netInfoOnline`, the Expo Router guards `usePermissionGuard` and `useSnapshotReady`, and the snapshot sources `localSnapshot` and `powersyncSource`. `permdock/vue` exports `permdockPlugin` plus the same composable names and `<Protected>`. `permdock/svelte` and `permdock/solid` keep the same surface under each framework's idiom.
* `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/prisma` also exports `permdockExtension`, `resolveRelated`, `toPredicate` and `prismaModelFields`. All three also export `withSubject` and `checkRow`. `permdock/jwt` exports `subjectFromJwt`, `createJwtSubjectResolver`, `verifyDpopProof`, `joseTokenVerifier` and `joseTokenSigner` because it produces subjects, verifiers and signers for a factory, not an instance. Structurally typed providers (`permdock/supabase`, `permdock/clerk`, `permdock/better-auth`, `permdock/convex`, `permdock/prisma`, `permdock/mcp`, `permdock/react-native`) declare **no peer dependency**: they duck-type the SDK you already installed. `permdock/supabase/middleware` is the exception on the Supabase side: it calls `defineMiddleware`, so `@supabase/middleware` is its optional peer, while `@supabase/server`'s `JWTClaims` is still duck-typed.
* `permdock/approvals` exports `memoryApprovalStore` and `approvalsHandler`, and `permdock/cloud` exports `cloud`; both produce values you pass *to* a factory (`store`, `sink`, `policies`) or a client provider (`source`) rather than a `PermDock`.
* `permdock/testing`, `permdock/testing/saas` and `permdock/testing/saas/permissions` export the test runners (`describePolicy`, the `test<Interface>` runners, `rlsParity`, `ormParity`, `testClientParity`, `testHttpAdapter`, `testAuthZen`) and the shared SaaS domain. Test files import them; application entries never do, and Vitest is an optional peer.
* `permdock/cli` exports `defineConfig` (for `permdock.config.ts`) and `run` (the `permdock` binary as a function) plus the config types, and `parseHookMarker` / `parseGrantsMarker` with their result types `SupabaseHookMarker` / `SupabaseGrantsMarker`. It is a build-time entry: no runtime entry imports it.
* Build hooks: `permdock/next/plugin` exports `createPermDockPlugin` and `permdock/unplugin` exports `createPermDockUnplugin` (Vite, Rollup, webpack, Rspack, esbuild through [unplugin](https://unplugin.unjs.io)). Both run `collect` at build time and never wire runtime API. 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).

## Names shared on purpose [#names-shared-on-purpose]

* `PermDock` methods and snapshot fields share a name when the field is the method's result serialized: `permdock.actions(resource)` and `snapshot.actions`, `permdock.audiences()` and `snapshot.audiences`, `permdock.snapshot()` and a `snapshot` option or prop, `permdock.tenant(id)` and `principal.tenant`. The method is on the instance; the field is the wire form a client hydrates from.
* One name can be exported by several entries with that entry's meaning, like `createPermDock`. `PermDockPluginOptions` is the build plugin's options in `permdock/next/plugin` and `permdock/unplugin`, and the Vue app plugin's options in `permdock/vue`.
* Every adapter returns `<Adapter>PermDock` from `createPermDock` and takes `<Adapter>PermDockOptions`, rather than `<Prefix><Thing>`, so the adapter is the first word wherever both appear (recorded in the repository's `docs/decisions/0019-adapter-suffix-and-no-dollar-members.md`).

## Spec names [#spec-names]

PermDock's subject vocabulary is TypeScript, camelCase and typed; the specifications it consumes are JSON claims with short registered names. [subject](/docs/concepts/subject) fixes the rule: where a spec name exists, PermDock either uses it verbatim or maps to it exactly once, in the table below and on [subject](/docs/concepts/subject); PermDock never coins a second spelling for a concept a specification already names.

| Specification name | Source | PermDock name | Rule |
| --- | --- | --- | --- |
| `sub` | RFC 7519, OIDC Core | `principal.id` | Opaque string, compared byte for byte |
| `iss` | RFC 7519, OIDC Core | `principal.issuer` | Always set by `subjectFrom*`; identity is `issuer` + `id` |
| `aud` | RFC 7519 | `audience` option | Verified, not stored |
| `exp` | RFC 7519 | `subject.expiresAt`, `Membership.expiresAt`, `ApprovalRequest.expiresAt`, snapshot `expiresAt` | NumericDate (Unix seconds) everywhere, the same unit as `exp`; the JWS envelope's `exp` copies it |
| `iat`, `nbf`, `jti` | RFC 7519 | none on the subject; `iat`, `jti` on signed outputs | Verified, then dropped |
| `act` | RFC 8693 | `actor` | Innermost `act.sub` is `actor.id`; the nesting is `delegation.chain` |
| `client_id`, `azp` | RFC 9068, OIDC Core | `actor.id` when the client acts under a delegation; verified otherwise | `actor.kind` `'oauth-client'` or `'mcp-client'` |
| `Signature-Agent` `keyid` | Web Bot Auth / RFC 9421 | `actor.id` when `webBotAuth` is set | `actor.kind` `'web-bot-auth'` |
| `scope` | RFC 6749, RFC 9068 | `delegation.scopes`; `permission.scope` | Space-separated string on the wire, array in TypeScript |
| `authorization_details` | RFC 9396 | `delegation.authorizationDetails`; `permission.authorizationDetails` | One RAR `type` per permission |
| `access` | RFC 9635, RFC 9767 | `delegation.access` | Objects and reference strings kept verbatim |
| `cnf` (`jkt`, `x5t#S256`, `jwk`, `kid`) | RFC 7800, RFC 9449, RFC 8705 | `binding` with the same member names (reshaped) | FAPI's "sender-constrained" gives the field its name; the members are the `cnf` members so it round-trips |
| `acr`, `amr`, `auth_time` | OIDC Core, RFC 8176 | `principal.assurance.acr`, `.amr`, `.authTime` | Provider `aal` maps into `acr`; RFC 8176 `amr` values include `pwd`, `otp`, `swk`, `hwk`, `mfa` |
| `verified_claims`, `verification`, `trust_framework`, `claims` | OpenID Connect for Identity Assurance 1.0 | `principal.assurance.verified[]` with `verification` and `claims` kept under the spec's names | The collection is `verified` because `assurance` already scopes it; the fields inside keep the spec's snake case |
| `aal` (`aal1`, `aal2`) | Supabase Auth JWT | `principal.assurance.acr` | Verified by `getClaims()`; `aal2` is MFA |
| `fva` / `factorVerificationAge` | Clerk session token | Recipe into `principal.assurance`; not mapped by `subjectFromClerk` | Second element `>= 0` means a second factor was verified |
| `twoFactorEnabled` | Better Auth user row | Recipe into `principal.assurance`; not mapped by `subjectFromBetterAuth` | A session exists only after the two-factor plugin verifies |
| `sid` | OIDC Front- and Back-Channel Logout | `subject.session` | The join key for `logout_token` and CAEP events |
| `roles`, `groups`, `entitlements` | RFC 9068 | `principal.roles`; `principal.memberships` (`team` from `groups`); `principal.plans` from `entitlements` | SCIM `value` only |
| `userName`, `externalId`, `active`, `displayName` (SCIM `User`) | RFC 7643 | `DirectoryUser.userName`, `.externalId`, `.active`, `.displayName` verbatim | `externalId` is the IdP's identifier and `id` is the store's; `active: false` yields no memberships; `displayName` is never an identifier |
| `members[].value`, `displayName` (SCIM `Group`) | RFC 7643 | `DirectoryGroup.members[].value` verbatim; `via: 'group:<id>'` on the membership | Group `id` is the group identifier; `display` and `displayName` are labels only |
| `urn:permdock:scim:schemas:extension:roles:1.0` (`roles`) | RFC 7643 section 3.3 extension | The `Group` extension carrying the declared role names for a group; read by `directoryMembershipSource` | Unknown or non-`assignable` role names are dropped, never invented |
| `type`, `source`, `subject` (CloudEvents) | CloudEvents 1.0 | `dev.permdock.decision`, `dev.permdock.approval`, `dev.permdock.directory`, `dev.permdock.membership`, `dev.permdock.catalog`, `dev.permdock.access.started` / `.ended` / `.revoked`; the emitting service; the permission key, resource id or principal id | The envelope for every webhook and queue sink ([wire formats](/docs/concepts/wire-formats)) |
| `jwks_uri`, `issuer` (Discovery) | OIDC Discovery, RFC 8414 | `discovery` option; `jwks` and `issuer` options | Discovery supplies both |
| `typ` (`at+jwt`, `JWT`, `logout+jwt`, `secevent+jwt`) | RFC 9068, RFC 7519, OIDC, RFC 8417 | `accept` option; `permdock-snapshot+jwt`, `permdock-approval+jwt`, `permdock-decisions+jwt`, `permdock-policy+jwt`, `permdock-capability+jwt` on outputs | Explicit typing, RFC 8725 section 3.11 |
| `invalid_token`, `insufficient_scope`, `insufficient_user_authentication` | RFC 6750, RFC 9470 | `on('auth')` reason `invalid-token`; Decision reasons `not-delegated` / `no-delegation` and `insufficient-user-authentication` | Hyphenated in PermDock, underscored on the wire; the HTTP adapter renders without a second table |
| `Subject`, `Resource`, `Action`, `Context` | AuthZEN 1.0 | `principal` (+ `actor`, `delegation`, `tenant` under `context`), `resource`, `action`, `context` | The evaluation mapping on [AuthZEN](/docs/standards/authzen) |

## Files and folders [#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`. The first two carry `digest` (React's name for the property that survives the Server Component boundary): `PERMDOCK_DENIED;<permission>` and `PERMDOCK_APPROVAL_REQUIRED;<permission>;<token>`, read with `parsePermDockDigest`.

## Renamed before 0.1.0 [#renamed-before-010]

| Old | New |
| --- | --- |
| `CreatePermDockOptions` | `PermDockOptions` |
| `CreatePermDockPluginOptions` | `PermDockPluginOptions` |
| `ServerPermDock.handler`, `AuthzenPermDock.handler` | `permdockHandler`, like every other HTTP adapter |
| `ExpressPermDock.handler(fn)` | `withPermDock(fn)`, like `permdock/convex` |
| `SsfAdapter`, `SsfOptions` | `SsfPermDock`, `SsfPermDockOptions` |
| `SupabaseMiddlewareOptions` | `SupabaseMiddlewarePermDockOptions` |
| `A2AAgentCard`, `A2APermDock` and the other `A2A*` types | `A2aAgentCard`, `A2aPermDock`, … (`A2a` like `Ssf` and `Mcp`) |
| `dock`, `pd`, `server`, `factory` for the instance in examples | `permdock`; a `cloud()` result is `permdockCloud` |
