Changelog
Every PermDock release, newest first. Per-package detail is in packages/permdock/CHANGELOG.md.
0.3.1 (2026-10-11)
permdock rls generateandpermdock supabase hook generateread global roles only from the rows without a tenant whenrls.rolesreferences a roles table throughthroughandrls.customRoleWrites.rolesnames that table with atenantcolumn.permdock_replace_global_rolesresolved keys over every row, so replacing a user's roles with['dispatcher']inserted one row per tenant role nameddispatcherand kept rows that referenced a tenant role. It now resolves, keeps and inserts only global rows, and a key only tenant roles have raisesunknown-role.permdock_hasand the token hook no longer read a global-roles row that references a tenant role as a global role.
0.3.0 (2026-10-10)
decideRoleChangechecks a change to a global role. Passscope: 'global'and noidfor a role declared withouton;RoleChangeis now a union in whichidis required for a named scope and absent for'global', so a helper typedPartial<RoleChange>needsExtract<RoleChange, { id: string }>. The authority is the oneassignableRoles({ scope: 'global' })lists, which is new: the assignable global roles the subject may hand out through its own global roles.exclusiveWith,managedBy: 'idp'and the self-change rule apply as for a scoped role, a role held at a named scope is denied withscope, and anidon a global change is denied withvalidation. The snapshot-backed client answersassignableRoles({ scope: 'global' })with an empty list.permdock rls generate --seeds-out <directory>numbers a new seeds migration one second after the newest versioned migration in the directory instead of using the current time, so regenerating the same directory gives the same file name and--checknames the same missing file on every run. The clock is read only when the directory holds no versioned migration. A seeds migration still sorts after the migrations that createrole_permissions, and an unchanged policy still writes nothing.permdock rls generatewritespermdock_replace_global_roles(p_user, p_roles text[])whenrls.rolesnames the global-roles table. It deletes the user's rows whose role is not in the list and inserts the missing ones in one call, for a role editor that saves a whole set. It runs with the caller's rights, so the table's policies and therls.assignmentstrigger judge each row, andexecutegoes toauthenticated. A role column that references a roles table (through) refuses an unknown key withunknown-role. Regenerate the helpers.permdock/better-supabaseaccepts better-supabase 0.7 as its optional peer, so an app can install better-supabase 0.7.0 next to PermDock with strict peer dependencies.permdock/better-supabase:bucketPolicyandtopicPolicyaccept{ policy, schema?, scope, segment? }besides{ manifest, catalog, scope, segment? }. With thedefinePolicyresult the app already imports, no manifest or catalog is read at runtime: the scopes, the permissions with row conditions and the scopes a permission is granted at come from the policy, andschemanames the helper schema (defaultpermdock). The same keys are refused as before.supabase.hook.api: { schema?, prefix? }makespermdock supabase hook generatewritepublic.permdock_subject_for,permdock_members_ofandpermdock_authz_version_for(schemapublicand prefixpermdock_by default), so a backend can call the hook's readers through the Data API without hand-written wrappers. Each issecurity definer, revoked frompublic,anonandauthenticated, and granted toservice_role: the only grant the hook makes to it, and only withapi.postgrestSources(client, { api })takes the same setting and derivesschema,fn,membersFnandversionFnfrom it; the four options still override.
0.2.0 (2026-10-10)
-
A
createPermDockinstance asked for a tenant other than the one a user key is held to now answers for no tenant: it keeps none of the owner's memberships, reads no entitlements and denies every check, soprotect(),can(),where(), snapshots andtenants()agree. Before, the key's tenant took precedence over the requested one, and a route of tenant T granted a permission the owner held in the key's tenant. This covers every adapter, the MCP and agent entries and the remote PDP, which all resolve the tenant throughcreatePermDock. -
A
usercredential may name atenant, which holds a personal key to one tenant of its owner.parseCredentialaccepts it (it was rejected before),decideCredentialtakestenanton a user request and refuses one the creator is not a member of withexceeds-creator, and the tenant'scredentialssettings govern the key.createPermDockmakes the key's tenant the active tenant, keeps only the owner's memberships inside it and drops the owner's global roles, whether the memberships come fromowner, the token or amembershipssource, socan(),decide(),where(), snapshots andtenants()answer for that tenant only.rls.apiKeysalready holds a key whose claim names a tenant to it in SQL; a verifier copies the credential'stenantinto that claim.idsstill holds resource ids only. -
A credential may list any number of permissions.
parseCredentialanddecideCredentialno longer cappermissionsat 64 entries, so a verified key that delegates a read-and-write set over a catalog of 100 or more permissions resolves throughsubjectFromApiKeyinstead of failing asinvalid-claimsand becoming anonymous. The cap guarded self-contained tokens; a credential always comes from the application's own store through a verifier.idsper entry keeps its limit of 64. -
A membership role column that stores a role id is read as a key through a roles table:
fromTablecolumns.role,fromJunctionroles, anrls.membershipstable'sroleandauthorizeSql'stenant.roletake{ through, on, column }, the shaperls.rolesalready takes. The token hook, in-processmembershipsForandlist, thedatabasemode helpers, the custom-role match, the ownership triggers,permdock_can_assignandmemberOfall join the roles table,supabase_auth_admingets read access to it,permdock_bump_authz_version_role_keysbumps every global and membership holder of a renamed key, the manifest's membershiprolecarriesthrough, and doctor PD028 counts the roles table's id and key columns.permdock_can_assignnow also answers from membership sources indatabasemode. -
A membership row can hold roles in several columns:
roleinrls.membershipsandfromTablecolumns.roleaccept an array of columns and{ through, on, column }references, andfromJunctionacceptsroles: { sources }. The row holds every non-null key in the token hook,membershipsFor, the database-mode helpers and the holder-count triggers. -
A membership source can read its user through another table:
fromJunctionuserandfromTablecolumns.usertake{ through, on, column }, the shape a role column already takes, so portal contacts incustomer_contacts (contact_profile_id)whose login iscontact_profiles.user_idneed no copieduser_id. The token hook, in-processmembershipsForandlist, thedatabasemode helpers andpermdock_can_assignjoin that table and filter on its user column, withsuspensionapplied to the joined user and to the source's instances. A row whose referenced row is missing or has no user holds no membership.supabase_auth_admingets read access to the table,rls generate --indexesindexes its user column,permdock_bump_authz_version_member_usersbumps the users a membership row references, andpermdock_bump_authz_version_linked_usersbumps both the old and the new user when the referenced row is re-linked, skipping a login that was deleted. The manifest's membershipusercarriesthrough, and doctor PD028 counts the referenced table'sidand user columns. User and rolethroughcombine on one source. -
A policy delegation can name an OAuth client by a stable name,
to: { kind: 'oauth-client', client: 'cli' }, instead of its id.subjectFromSupabase,permdock/mcp(createPermDockandsubjectFromMcp) andsubjectFromJwt'sactoroption takeclients(ClientNames: a record from name to id, or a function from a verified id to a name) and setactor.client. An id the mapping does not name, or names twice, leavesclientunset and the delegation does not apply. The catalog's delegationtogainsclient. -
A policy's
MapandSetmembers (rolesByName,resources,index.grantsByKey) now throw aTypeErroronset,add,deleteandclear, as the rest of the frozen policy does on assignment.memoryRoleSourcereturns a copy of its role list on each call. -
A resource whose rows are the scope's instances, such as an
organizationstable whose ownidis the organization id, can take scoped roles: its onememberOfrelation to the scope ({ self: { field: "id", memberOf: "organization" } }) names the field when it is not the scope'skey.can,where(),whoCan, snapshots andrls generateread that field; a snapshot'sscopes[]entry carries it in the new optionalfieldsmap.definePolicythrows when a resource has severalmemberOfrelations to a scope and none on its key. -
A route that checks no permission can now require OAuth scopes.
protect(null, loadData?, { oauthScopes })inpermdock/serverand the Hono, Express, Fastify, Elysia and Node adapters,withPermDock({ oauthScopes })withoutprotectin the Supabase middleware, and anoperationPermissionsentry with onlyoauthScopes(read throughoperationsonpermdock/server) gate a route such as a chat endpoint: an anonymous caller gets401, a delegated token holding none of the scopes gets the same403insufficient_scopechallenge as a permission route, and a first-party session passes. The guard is aScopeGuard, with nodecision. An app no longer needs its own rule that maps HTTP methods to scopes. Routes with a permission behave as before. -
A single membership can be suspended while the user's other memberships and sign-in stay intact.
disabledAtnames a nullable timestamp column on anrls.membershipstable, onfromTable(columns.disabledAt) and onfromJunction; a row with a value keeps its role, and only its permissions drop.rls.suspension.memberships.keeplists what a suspended membership still holds, as a scope'skeepdoes. Thedatabasemode helpers, the inlinememberOfchecks,authorize(), the token hook,subject_for,members_ofand the in-process sources honour the column, thejwtmode helpers andauthorize()honour thekeepof a claim membership, and the holder-count triggers andpermdock_can_assigncount no suspended membership. The manifest carriesmemberships[].disabledAtandrls.suspension.memberships.keep, and PD061 lists the kept keys. -
A snapshot check reads the clock only when a grant needs it. A
usePermissionhook under aPermDockProviderthat is still awaiting itssnapshotPromiseno longer callsDate.now(), so it renders in a Next.js Cache Components static shell without a Suspense boundary. -
A snapshot instance's
can(),filter()andpick()skip the decision token and the deep freeze, and the UI stores reuse a local decision for the rest of one render pass.useTenant,useMemberships,useRoles,useAssignableRoles,useAssignablePermissionsanduseSubjectreturn the same object until the store changes. -
A snapshot now carries
ids, the row id field of each resource whoseidoption is notid. A clientcanon a membership held on one row then answers as the server does, where it used to deny. The clientcan()returnsfalseinstead of throwing when reading the row throws. The plan seats a client counts now use the policy's scopes. -
A snapshot-backed instance (
fromSnapshot) now answers an instance action checked without a row the way the server does: from the first-scope memberships of the active tenant, or from the instanceteam(id)selected. It used to deny every such check on a partitioned resource withtenant-mismatchorscope, so a page guard that passed on the server failed on the client. -
A suspended scope can keep permissions.
rls.suspension.scopes.<scope>.keep(and the same key onSupabaseSuspensionforfromTable,fromJunctionandauthorizeSql) lists permission references or keys that members of a suspended instance, and of every instance nested in it, still hold through their roles: cancelling a scheduled deletion, restoring the instance, exporting before a purge. The generatedpermitted_<scope>_idshelpers and their_forforms compare the grant's permission with the kept keys in both modes, inline policy checks drop the suspension check for a kept permission,authorize()answers a kept permission for a suspended tenant, and the membership sources, the token hook,subject_forandmembers_ofkeep the rows of a suspended instance with akeeplist.Membership.keepcarries it in process: such a membership counts only for the kept permissions, never for role listings or assignment checks, and a malformedkeepdrops the membership.subjectFromSupabase, the JWT claim mapping, the claims schema and the hook manifest read the new field.permdock doctorPD061 lists each scope's kept keys and warns on a key the definitions do not declare. Withoutkeep, the generated SQL is unchanged. -
Add the
{ subject: { session: { live: true } } }condition, which holds only while the Auth server still holds the session:subjectFromSupabase(claims, { liveSession: true })sets it in process, andrls generatecompiles it topermdock.permdock_session_live(), which readsauth.sessions(Supabase dialect only). -
An
ApprovalPolicyentry withwhereon a collection permission no longer loads silently and never matches: it is refused like any other invalid entry, so the source denies withapproval-policy-unavailableuntil the entry usescheck. The newvalidateApprovalPolicy(policy, entry)returns{ ok: false, problem }(unknown-permission,invalid,where-on-collection,stale-on-without-version) so an application can refuse a bad entry when it is saved. -
An
rls.membershipstable mapping takes a constant membership kind:via: { value: 'staff' }gives every row that kind, the wayfromJunction'sviadoes, so a staff table needs no kind column (or a generated column) for roles withfor: ['staff']to grant anything. The helpers, theexistschecks ofmemberOf, the ownership triggers andpermdock_can_assignread the constant, and the hook manifest describes it asvia: { value }.via: '<column>'keeps naming a column. -
An instance check without a row (
can(permission, undefined)in a page guard) now answers from memberships of the first scope only. A membership of a nested scope, such as a portal contact'scustomermembership inside an organization, applies only when the row carries that scope's key or whenteam(id)selected its instance; otherwise the check isdeniedwithscope. This is a breaking change for an app that relied on a nested role passing an organization-level guard: pass the row, or select the instance withteam(id). Snapshots already denied these checks and are unchanged. -
Approvals gain
holder(permission), an approver who holds the permission in the request's tenant through any role (custom roles included), checked throughverdict.permissionsthatapprovalsHandlerreads with the newpermdockForoption andapproverPermissions.anyOf(...)lets any one of several approvers sign (a list, orallOf(...), stays all-of). A stage can carry its ownescalation, and anApprovalPolicyentry may now setescalation, which widens only that entry's stages. -
Breaking:
PermDockProviderfrompermdock/react-nativerequiressubjectId, the signed-in user's id ornullwhen signed out. A stored snapshot is used only when its principal matchessubjectId;nullclears the storage. With averifier, the provider stores the signed JWS and verifies it again at launch instead of storing the decoded snapshot. A stored copy starts withstatus: 'stale'until the first refresh. Storage that throws or rejects no longer breaks the provider. Newheadersno longer rebuild the store; the next refresh sends them. The interval pauses in the background, a return to the foreground refreshes, and the newsubscribeOnlineprop skips refreshes while offline. -
Breaking:
PermDockProviderfrompermdock/reactno longer suspends its readers on asnapshotPromise. Until the promise resolves,usePermission,usePermissions,usePermDockand the other hooks answer from the current snapshot withstatus: 'pending'(an empty snapshot denies),<Protected>renders itspendingslot, and the store re-renders the readers when the promise settles. Permission UI therefore stays in the Next.js Cache Components static shell, as the guide describes. A promise React has already settled hydrates before paint, and a rejection fails closed withstatus: 'server-only'instead of reaching an error boundary. To keep the previous behavior, passsuspendtoPermDockProviderfrompermdock/reactorpermdock/next. -
Breaking:
subjectFromBetterAuthfrompermdock/better-authno longer copies the user's additional fields intoprincipal.claimswithoutoptions.schema, because a user can edit their own additional fields. Pass a Standard Schema that lists the server-set fields to keep:subjectFromBetterAuth(auth, session, { schema: z.object({ plan: z.string() }) }). -
Breaking: a thrown
assertnow answers with the statusprotectsends for the same decision.toProblemDetails()andproblemFromError()return 401 for no subject or a missing step-up, 404 for a row of adisclosure: 'hide'resource, 429 with theRateLimitfields for an exhausted limit and 503 for an unavailable limit store, where they returned 403.problemFromError(error, { credentials })picks the401challenge. The AuthZEN and approvals 401s carryWWW-Authenticate, Nest's missing-row 404 and the Supabase middleware's 405 carry Problem Details, and Convex mapsPermDockValidationError. -
Breaking: every URL PermDock emits moves from
permdock.devtopermdock.com: Problem Detailstypevalues (https://permdock.com/problems/<slug>), the$idand$schemaofcatalog-v1.json,supabase-claims-v1.jsonandsupabase-manifest-v1.json, and the docs links in errors and CLI output. A client that matches on atypeURI or a schema$idupdates the host. -
Breaking: two fixtures in
permdock/testingdrop the better-supabase name, with the same contents.Before After supabaseClaimFixtures.betterSupabasesupabaseClaimFixtures.tenantPlansbetter_supabase.feature_claimsinsupabaseHookManifestFixturepublic.feature_claims -
Deleting an
auth.usersrow no longer fails when its memberships cascade. The generatedpermdock_bump_authz_version_forandpermdock_bump_authz_version_role_keysnow skip a user that is no longer inauth.users, so the version trigger on a membership table cannot insert apermdock_authz_versionrow that violates its foreign key. Regenerate the hook SQL and apply the changed function body in a migration. -
Doctor PD039 and
supabase hook generatenow find helpers written withrls generate --split. They read thehelperspart of anrls.outpath with a{part}placeholder, and the SQL files in the hook file's folder, besidesrls.outand the migration folders. Before, a project that kept the split path in a script got a false warning that the helper schema had no helpers. -
Edge
groupsaccept a subject column per group:groups: { resources: { team: { relation: 'member', subject: 'team_id' } } }. Share tables that keep each subject kind in its own typed column (user_id uuid,team_id bigint) no longer need one subject column plus a kind column. Withoutcolumn, a row names the principal when the edge'ssubjectis not null and a group when that group's column is not null.memoryRelations, thewhere()compilers andpermdock rls generateall read the new form, and theEdgeGrouptype is exported. A plain relation name per group still works and still needscolumn. -
Every
customRolesoption, oncreatePermDockand on each adapter (permdock/server,permdock/mcp,permdock/next, the agent adapters and the rest), also takes aRoleSourceFactory,(subject) => RoleSource | undefined, called once per instance with the resolved subject. A source that reads one principal's roles no longer has to remember the last principal an adapter served; a factory that throws reads no custom roles and reportssource-threw.RoleSourceFactoryis exported frompermdock. -
Every adapter's options type now extends the exported
InstanceOptions, soapprovalPolicies,relations,entitlementsandpoliciesreach the instance from every adapter; before, only the core factory readapprovalPolicies, so approval rules kept as data never applied to adapter decisions andtoolApprovalcould not returnuser-approvalfor them. The Hono, Express, Fastify, Elysia, Nest, Node, tRPC and oRPC adapters acceptactor, and tRPC and oRPC acceptotel.Breaking: the
snapshotsoption is removed from every server and agent adapter factory, which never read it. Delete it from the options object; aSnapshotSourcesuch ascloud().snapshotsgoes to thesourceoption ofpermdock/react-native. -
Generated RLS renders
notand deny policies as(…) is not true, so a row whose deny condition is NULL stays visible, ascan()allows it. Runpermdock rls generateagain to pick this up. -
Grants take
group, a stable name for their condition group in generated SQL:allow(permissions.quote.read, { where: { status: 'sent' }, group: 'sent' })gives the grant keyquote.read#sentinstead of a positionalquote.read#2, so hand-written SQL that repeats the condition keeps working when other grants are added, removed or reordered. Unnamed groups keep their positional keys.rls generaterefuses one name on two conditions of a permission and two names on one condition, anddefinePolicyrefuses a name that is not lower case letters, digits,_and-starting with a letter, or isbreak-glass. The option is ignored in process. Without it nothing changes. -
New
permdock/better-supabaseentry for better-supabase 0.6, which is an optional peer.authorizationProvider({ manifest, catalog, scope?, approver? })builds itsauthorizationconfig frompermdock.manifest.jsonandpermissions.catalog.json. Withrls.customRolesat a root tenant scope,canAssignandcanAssignForcallpermdock_can_assign_anyand its_forform, so better-supabase'scan_assignaccepts custom roles.permissionsForcalls the newpermitted_<scope>_permission_keys_forhelper thatrls.mode: 'database'writes.approversetscanApprovewithapprovals.distinctApprover. Scopes carry a normalisedidType, and a missing catalog is reported inproblems.bucketPolicyandtopicPolicybuild its storage and realtime access policies fromPermissionreferences.- The SQL templates call
permitted_<scope>_ids_by_permission,permdock_has_permissionand their_forforms, so a deny of the permission is subtracted and a permission split into#ngrant keys still matches. apiKeyVerifier({ keys, manifest?, serviceRoles?, allPermissions? })turns its API keys block into aCredentialVerifier;serviceRolesdefaults to the manifest'srls.apiKeys.serviceRoles. Keys needcreateApiKeys({ prefix: "pdk" }). A rotated key's credential expires when its grace period ends.apiKeyClaimOptionsreads itsapi_keyclaim settings fromrls.apiKeys.subjectFromBetterSupabase(session, options?)reads its sessions with thefeaturesclaim as plans;plans: { claim, keys }decodesentitlements.claim.keysshort codes, andapiKeysmaps anapiKeysession to the key's credential subject.toolPolicy({ permdock, data? })returns theauthorizeandvisiblehooks ofcreateMcpfor tools whosemetais a permission.credentialGuard(provider, { permdock, use, revoke? })wraps aCredentialProviderso PermDock decides each token use.
The Supabase manifest's helper names may now end in
_permission_keysand_permission_keys_for. Regeneratepermdock.manifest.jsonwithpermdock supabase inspect --out. -
New command
permdock configwarns on config keys PermDock does not read, and--printprints the effective config with every default. Every command now exits2when a module path or a config section has the wrong type. Under--json, a non-zero exit always prints JSON: a text failure such ascollect --checkdrift becomes Problem Details withtypehttps://permdock.com/problems/cli-failed.doctorloads the policy and scans the sources once per run. It reports a policy module that fails to load asPD059and a source file that does not parse asPD060. -
New entry
permdock/compileexportscompileWhereand theCompiledWheretree that the Drizzle, Prisma and Kysely compilers render.testWhereCompilertakes an optionalmatches(compiled, row)and then checks that a compiler selects the rows the in-memory evaluator keeps. -
On Supabase,
permdock rls generatewritespermdock_user_id()into the helper schema, and every generated helper, policy, field view and trigger reads the subject through it in every mode. It reads thesubofrequest.jwt.claimsand returns null for a token whosesubis an empty string, such as a tenant service key, whereauth.uid()fails the uuid cast.executeon the function follows the helpers' grants, and ananonpolicy that reads the subject now grantsanonthe helper schema asrls.anonExecutedoes. The Supabase manifest lists it inrls.helpers, andrls importreads it asprincipal.id. Regenerate and apply the migration.Breaking:
withSubjectinpermdock/drizzle,permdock/kyselyandpermdock/prismaandpermdock rls verifyno longer setrequest.jwt.claim.sub. A test or job that sets onlyrequest.jwt.claim.subsetsrequest.jwt.claimswith asubinstead. Doctor check PD062 reports migrations that still read or set arequest.jwt.claim.<name>setting. -
REST routes can now require the OAuth scopes an operation declares, as
permdock/mcptools can.protect(permission, loadData, { oauthScopes })in every HTTP adapter, oroperationsonpermdock/server(the result ofoperationPermissions), narrows which coarse scopes reach a route: when the subject carries a token delegation, a token holding none of them is refused before the loader runs with403andWWW-Authenticate: Bearer error="insufficient_scope"naming the first. A subject without a delegation, such as a first-party session, is not gated, and the decision still needs the delegation to cover the permission.operationPermissionsentries takeoauthScopes, read byoauthScopesForRequest(method, path)for REST andoauthScopesForOperation(id)forpermdock/mcp'soauthScopesFor, so one declaration narrows both. Routes without the option behave as before. -
Scope ids compare as text. A row whose key column is a number (a
bigintid read through supabase-js) now matches a membership whose id is that number's text, incan, snapshots,heldRoles({ id }), custom-role matching,decideRoleChange,activateand capabilities; before, the check denied withscope. A membership whoseid,withinoron.idis a number or bigint is read as its text instead of being dropped. -
Server decisions do less work per request. The decision endpoints look permissions up by key, build their handler once and reuse the request's subject; tRPC and oRPC subscriptions open with the row
protectloaded; Nest rules sharing aloadDatarun it once; Fastify routes opt out withconfig: { permdock: false }.withSubject(Drizzle, Kysely, Prisma) sets the role and claims in oneselect set_config(…)statement.subjectFromBetterAuthreads every member row in one adapter query, andcreateClerkSubjectResolver({ cache: { ttl } })caches each user'smemberships: 'all'list for up to 30 seconds. -
Snapshots carry the roles and plans in
vocabularyand the permissions inassignableas plain objects, withkindas an ordinary field. They held the policy's own leaves, whosekindis a non-enumerable property, so React refused the snapshot as a Server Component prop in development ("Only plain objects can be passed to Client Components") and a JSON round trip droppedkind, so a clientisRoleorisPlancheck failed on a parsed snapshot. -
The React, Vue, Svelte and Solid providers take
snapshotUrl, whererefresh()fetches a fresh snapshot; it defaults toendpoint. They used to drop it. -
The Supabase hook file now defines
authz_version_for(p_user uuid) returns bigint, the authorization versionsubject_forreports without reading roles or memberships, under the same grants.postgrestSourcestakesversionFn(defaultauthz_version_for) andmemberships.versioncalls it, soclaimsFirst(sources.memberships, { onStale: 'reread' })costs one cheap call per request and readssubject_foronly when the token is behind. Regenerate the hook file and, whenpermdockis not an exposed schema, add a wrapper for the new function; until thenversionfalls back tosubject_foras before. -
The Supabase manifest gains
requires, thesupabase/sdkcapability-matrix feature ids the setup depends on.permdock/testingexportssupabaseClaimVectors, the claim fixtures as a conformance vector file forauth.session.get_claims. -
The
permdock-wireskill now wires@supabase/serverinto Hono, H3, Elysia and NestJS through a pipeline bridge (toHono([withClaims(), withPermDock()]), thenc.var.permdock) instead of the deprecated@supabase/server/adapters/*, which are removed on 1 December 2026. -
The agent adapters keep every field of the
Actorthey resolve, as the HTTP adapters do. Before,permdock/ai-sdk,permdock/claude-agent,permdock/eve,permdock/openai,permdock/mcpandpermdock/a2akept onlyidandkind. Anactoroption that returnedreadOnly: truetherefore lost its read-only narrowing, and theclientnamepermdock/mcpreads fromclientsnever matched ato: { kind, client }delegation. -
The hook file defines
members_of(p_scope text, p_id text) returns jsonbnext tosubject_for: every live membership of one scope instance as[{ principal: { id }, membership }], read from the hook's membership sources with their expiry and suspension filters, executable by no client role.postgrestSources(client, { membersFn? })calls it formemberships.list, once per instance, socountHoldersandwhoCanwork over PostgREST. Regenerate the hook and grant the new function to your backend role the way you grantedsubject_for. -
The hook file now defines
subject_for(p_user uuid) returns jsonb: the global roles, memberships, authorization version and, indatabasemode withrls.customRoles, the held custom roles the hook would put in a token forp_user, or{ id, active: false }for a suspended user. No client role may execute it.postgrestSources(client, { schema?, fn? })inpermdock/supabasereads it through supabase-js once per user and returnsmemberships(withversion, forclaimsFirst),customRoles(principal)andsubject(userId), so a backend without aSqlQueryno longer re-implements the membership sources, the suspension filter and a custom-role reader with the admin client. -
The optional
@nestjs/common,@nestjs/coreand@nestjs/platform-expresspeers now accept Nest 11 (>=11).permdock/nestuses no API that is new in Nest 12, and CI runs its unit and integration suites against Nest 11 as well. An app that carries Nest 11 through another dependency no longer getsERR_PNPM_PEER_DEP_ISSUES. -
UI stores key endpoint answers for a row without an id by its content and read the row id from the snapshot's
idsfield, so two such rows no longer share one answer.useApprovalpolling resumes when a component subscribes again after a StrictMode or remount unsubscribe.<Protected tenant>renderspendingwhile the provider's snapshot is on its way.PermDockProviderkeeps its store in state, so React never rebuilds it for unchanged props. -
Under pg-delta,
rls generate --splitno longer writes therls.realtimeandrls.storagepolicies into the declarativepoliciespart. The Realtime and Storage services createrealtime.messagesandstorage.objects, so pg-delta's shadow database, and a local stack with either service off, has neither table andsupabase db schema declarative syncfailed withrelation "realtime.messages" does not exist. The policies now go in the--seeds-outmigration, each in adoblock that creates it only whereto_regclassfinds its table. With those policies, thepoliciespart under pg-delta needs theseedspart with--seeds-out. Other layouts are unchanged. -
PermDockProviderfrompermdock/reactandpermdock/react-nativekeeps its store when a re-render passes an inlineheadersobject,fetchfunction orverifier. Before, every parent render rebuilt the store, dropped the refreshed snapshot and cached answers, and on React Native refetched the snapshot.headerscompare by value;fetchandverifiercall the latest prop. -
allow(permission, { requires })makes a grant count only where the subject also holds other permissions through a role:allow(drive.read, { to: relation(drive, 'viewer'), requires: file.read })keeps a share to the organizations where the user holdsfile.read, andrequires: [file.read, file.download]asks for every listed permission. Each requirement is met by a role grant without a row condition, declared or custom, globally or on the row's scope instance, minus a deny of that permission at the same instance. It folds into the grant's row condition forcan,where()and snapshots, andpermdock rls generateANDs onepermdock_has_permissionorpermitted_<scope>_ids_by_permissioncheck per key.Grant.requireson a normalised grant is areadonly string[]; the catalog lists one key as a string and several as an array.definePolicyrejectsrequireson a deny, on a collection action and for an undeclared permission.A delegated caller (an OAuth client or an API key) uses an allow with
requiresonly when its scopes cover every required permission and, unless the granted permission is read-only (meta.readOnly, or thereadandlistactions) and the allow is not a role grant, the granted permission as well.can(),decide(),where(), snapshots and therls.apiKeyschecks in the generated SQL apply the same rule. A key stored withfile:readkeeps reading the drives arequires: file.readshare reaches after an app movesdrive.readto relation grants, and a read-only key cannot write through an editor share. -
assignableRoles()no longer offers a custom role that allows nothing after the ceiling (empty, deny-only, or with every entry outside the ceiling or undeclared), anddecideRoleChangedenies assigning one withnot-assignable-by. Revoking such a role stays granted for an actor who may assign at its scope. -
assignableRoles()now lists the tenant's custom roles from theRoleSourcethat the subject may hand out, anddecideRoleChangeassigns and revokes them instead of denying withunknown-role. A custom role qualifies when the subject may assign a declared role at its scope (or holdsmeta.manageRoles) and may hand out every permission and level it allows; it has no holder count and inheritsforandexclusiveWithfrom its included roles. Declared roles behave as before. -
can,decide,assertandexplainaccept a permission typed as plainPermission, such as one fromlistPermissionsorfindPermission, when the call passesdata(undefinedfor a check without a row). Apps no longer cast the instance's methods to call them with a permission chosen at run time; literal references keep their narrower overloads, so an instance action without a row still fails to compile. -
claimsFirst(sources, { version, onStale: 'reread' })re-reads the memberships from the sources when the token'sauthzVersionis behind the source'sversion, or absent, instead of keeping the token's memberships and denyingfreshpermissions. A token minted before a new membership or contact link then sees it on the next request without the application rebuilding the instance. A fresh token costs oneversionread and no membership read; aversionthat throws keeps the token's memberships and marks the subject stale. The default,onStale: 'deny', keeps today's behaviour. -
cloud().approvals.resolvethrows theApprovalErrorcode and message the Cloud sends, such asapprover-not-eligible. It used to report every refusal other than409and410asapproval-not-found. -
countHolders(memberships, { scope, id, role })counts the principals holding a role in one scope instance from theMembershipSource'slist, live memberships only and each principal once, for theholdersthatdecideRoleChangeneeds withmin,maxortransferOnly. It answersundefinedwhen the source cannot list or the read fails, which the role change check denies. -
createPermDock({ secretKeys })inpermdock/supabase/middlewareturns a Supabase secret key thatwithSupabasematched by name into a tenantserviceprincipal capped at the declared permissions, andwithSubjectnow writes therls.apiKeysclaim (sub: ''for a service key) for any principal that acts through a credential.fromSupabasePostgres(ctx.postgres)inpermdock/supabaseadaptswithPostgresClient's client to theSqlQuerythatfromTable,fromJunctionandsupabaseApprovalStoretake. -
createPermDockfrompermdock/serverreturnsgetSnapshot(request, { tenant, include, tenants })for React Router, TanStack Start, SvelteKit and Nuxt loaders, reusing the request's cached subject.snapshotHeaders(snapshot, { tags })returns a privateCache-Controlcapped atexpiresAt,Vary, anETagandCache-Tag.cacheLifeForandsnapshotTagare now exported frompermdock/serveras well aspermdock/next. -
createPermDocknow callsRoleSource.globalRoleswith{ held }, the global role names the subject holds, andcustomRoleSource(reader, { read: 'held', policy })skips the platform read while every one of them is a declared role, as it already did for a tenant's roles. A request from a subject with only declared global roles no longer reads platform custom roles. Sources that ignore the argument are unaffected. -
createPermDocknow passesRoleSource.rolesFor(tenant, { held })the role names the subject's live memberships hold in that tenant, nested scopes included. A source whose custom roles live in a database can return[]without reading when every held name is a declared role. The parameter is optional in the type, so existing sources and direct callers keep working. -
customRoleSource({ rolesOf, assignable?, globalRoles? }, { read? })builds aRoleSourcefrom a read of every custom role of a tenant, soassignableRoles,assignablePermissionsanddecideRoleChangesee roles the subject does not hold. It keeps only the requested tenant's roles;read: 'held'with apolicyskips the read when the subject holds only declared roles.testRoleSourcetakesevery, the custom role names a tenant has, and fails a source that returns fewer with nothing held. -
databasemode addspermdock_can_assign_for(p_user, p_role, p_scope_id), theassignscheck ofpermdock_can_assignfor a user the caller names, and, withrls.assignmentsand custom roles,permdock_can_assign_custom_role_for(p_user, p_tenant, p_scope, p_scope_id, p_role)over the new internalpermdock_custom_role_guard_forandpermdock_custom_role_beyond_for. Trusted SQL that acts later for a stored user, such as accepting an invitation, re-checks that the inviter may still assign the role. No client role may execute them. The custom-role form is written only where every scope hasmember_<scope>_ids_for(Supabase). -
decide,can,assertandfiltertake ascopeoption naming a declared scope: only memberships of that scope answer the check, global roles still apply, and any other membership is skipped withscope. Use{ scope: 'organization' }for a staff guard on a collection action (quote.list), which by default also passes through a nested membership such as a customer contact's. The default is unchanged. Snapshot instances apply the option too. -
definePolicy({ oauthScopes })maps coarse OAuth scopes an authorization server issues (mcp:read) to the permissions they cover. A token holding one is delegated those permissions in every decision, snapshot and listing,permdock/mcpandpermdock/a2aaccept it in their scope pre-checks, andinsufficient_scopechallenges (MCP, A2A and the server kernel) name the first coarse scope that covers the permission instead of a<resource>:<action>scope the server cannot issue. -
describe(decision, { messages })acceptsmessages.reasonsas a function(reason, decision) => string | undefinedas well as a record, so a translation layer can read the denied decision.undefinedkeeps the reason code. -
doctor.srcPathsets the directories or globspermdock doctorreads for its source checks (PD001, PD002, PD007 to PD015, PD044 and the others over code), so client components outsidecollect.srcPathare checked without changing the catalog. It defaults tocollect.srcPath; PD003 and PD004 still follow the catalog. -
inherit(permission, { through })is a grantee that holds on a row when the subject holdspermissionon the row a link list or the parent points to:allow(node.read, { to: inherit(drive.read, { through: ['drive'] }) })makes a node readable wherever its drive is, by any grant ofdrive.read. In process the instance reads the target row through the new optionalRelationSource.row(implemented bymemoryRelations) and decides it with the full policy; a source withoutrowdenies withrelation-unavailable.permdock rls generatecompiles it to a check against the target'spermitted_<resource>_rows, which it now writes for every targeted resource.definePolicyrejects it on a deny, on a collection action, for an unreachable or undeclared target and for a cycle.testRelationSourcechecksrowwhen a source has it. -
localSnapshot({ manifest, read, subscribe })inpermdock/react-nativebuilds the snapshot from the device's own membership and custom-role rows, and the provider takes it assource.localSnapshotManifest(policy)frompermdockwrites the JSON manifest at build time, so the policy never ships to the app. -
operationPermissionsmatches aHEADrequest to theGETentry of its path when noHEADentry matches, soprotect(null)and a permission gate no longer fail on theHEADrequests a framework such as Next.js serves with theGEThandler. A declaredHEADentry still wins. -
permdock collect --watchcollects again when a source folder, the config file or the configuredpermissionsorpolicymodule changes. The Next.js plugin andpermdock/unpluginnow pick up an editedpermissions.tsinstead of reusing its first import, ignore the catalog and barrel they wrote, and debounce reruns to one at a time. -
permdock doctorPD001 now flags a client file importing any server-only entry, includingpermdock/express,permdock/drizzleandpermdock/otel. PD007 now counts every HTTP, MCP and agent adapter entry, includingpermdock/trpc,permdock/a2aandpermdock/webmcp. -
permdock doctorPD001 now reports a client entry that imports the configuredpolicymodule, resolving each import from the importing file: relative paths, tsconfigpaths, and package names throughnode_modulesand the package'sexports, so a workspace package that exports the policy (@acme/access/permdock/policy) is followed to the policy file. Type-only imports are not reported. -
permdock doctorPD002,collectandusagenow resolve a reference's root identifier to its binding. A parameter, local variable or module variable namedpermissions(an array of strings, for example) is no longer read as a permission tree, sopermissions.lengthis not reported as an unknown permission; an import ofpermissionsor of anydefinePermissionsexport, and adefinePermissionsdeclaration, still are, in any file order. -
permdock doctorPD004 now compares the catalog on disk with the catalogpermdock collectwould write fromcollect.srcPath. Before, it built the comparison fromdoctor.srcPath, so adoctor.srcPathwider thancollect.srcPathreported catalog drift thatpermdock collectcould never clear. PD002 and PD003 still count the references underdoctor.srcPath. -
permdock doctorPD005 looks for the skills and their lock in the working directory and every directory above it up to the workspace root, and accepts theskillsCLI'sskills-lock.jsonwhen it lists thepermdockskill, so a package in a monorepo whose skills are installed at the root no longer reports them missing. -
permdock doctorPD014 now reads only the options of PermDock calls: object literals passed to a function imported from apermdockentry, the object literals nested in them, and aconstobject such a call names. An object with ajwkskey that never reaches PermDock, such as another library's environment object, is no longer reported. Within PermDock options,discoverynext toissueris now reported as the docs describe, asdiscoverynext tojwksalready was. -
permdock doctoradds PD065: it warns when a file that importspermdock/react-nativereads a permission whose grant needs the server, because the device denies it offline. -
permdock doctorreports PD063, an error, for a migration that alters or drops a reserved Supabase role or grants a reserved membership, which supautils rejects.permdock rls generaterefuses to write such a statement. -
permdock doctor,collectandusageno longer throw on aforloop with an empty initialiser (for (;;),for (; test; )) or on an array hole in asnapshot({ include })list. The PD002 scanner reads everyfor,for...in,for...ofandfor awaitform. -
permdock powersync generatewrites PowerSync Sync Streams (sync-config.yaml, edition 3) from the policy andrls.memberships, andpermdock powersync verify --dbfails when a stream syncs a fixture row the policy denies. A resource with a deny grant gets no stream. Thepermdock_*streams sync the signed-in user's own membership and global-role rows, the roles tables they reference and, withrls.customRoles, the custom roles of the user's tenants: the rowslocalSnapshotneeds.powersync verifypasses a fixture'ssubject.claimstoauth.parameter()and fails when an own-rows stream syncs another user's rows.With
powersync.manifestset, generate writeslocalSnapshotManifest(policy)to that path. Doctor check PD058 flags a stale streams file or manifest and reports a config that does not compile.powersyncSource(db, { manifest, queries, read })inpermdock/react-nativebuilds the local snapshot from PowerSyncdb.watchqueries. -
permdock rls generate --custom-rolesindatabasemode emitspermdock_replace_custom_role_grants,permdock_rename_custom_role_grantsandpermdock_delete_custom_role_grants. They save, rename and delete a custom role's rows with the rules ofvalidateCustomRoleandassignablePermissions, checked against the signed-in caller, so an application no longer hand-writes asecurity definerfunction for it. A null tenant with scopeglobalwrites a platform custom role, checked against the global ceiling and global grants, and a caller holding ameta.manageRolespermission through a global role passes the membership check for any tenant.The
permdock_trusted_replace_custom_role_grants,permdock_trusted_rename_custom_role_grantsandpermdock_trusted_delete_custom_role_grantsvariants keep the definition checks without the caller checks, for migrations, jobs and backends; only the owner may execute them until a migration grants a role. -
permdock rls generateaddspermdock_permission_keys(p_scope)next topermdock_permission_keys(): the declared keys some declared role is allowed on that scope (globalor a scope name), so a role editor at the organization lists only what an organization role can hold. The zero-argument form is unchanged. -
permdock rls generatefolds a constant membership kind (rls.membershipsvia: { value }, or a table with novia) at generation time instead of emitting a per-rowcaseover every role withfor: a role the kind allows gets no filter, and one it excludes is filtered out by name. Access is unchanged. -
permdock rls generatenames graph helpers after the resource's snake_case name, so a camelCase resource such aschatThreadgetspermitted_chat_thread_ids,permdock_link_chat_thread_<link>andpermdock_closure_chat_threadinstead of failing with "unsafe scope name". Link names are converted the same way. Doctor PD032 now also reports two resources that share one snake_case name. Lowercase resource names generate the same SQL as before. -
permdock rls generatenow emitspermdock_role_permissions(p_role, p_scope, p_tenant, p_scope_id)andpermdock_permission_keys(), so role editors and admin screens no longer hand-write the join overrole_permissionsand the custom-role tables. The first returns the permission keys a role holds on a scope withallowordeny: a declared role fromrole_permissions, and withrls.customRolesindatabasemode a custom role of the caller's tenant, resolved through the ceiling asresolveCustomRoledoes. Only a member of the tenant (or ameta.manageRolesholder for a platform role) may read a custom role. The second lists every declared permission key. Both are executable byauthenticatedand not byanon. The change adds two functions to the helpers file and breaks nothing. -
permdock rls generateputs themin,maxandtransferOnlytriggers on the tables offromTableandfromJunctionmembership sources when a scope has norls.membershipstable, so a policy with ownership rules can keep its memberships in sources. Onepermdock_holders_<scope>and onepermdock_transfer_only_<scope>function count holders over every source of the scope, andMembershipSql.holders(typedMembershipHolders) describes the rows they count. A source withoutholdersstill leaves the counts todecideRoleChange. -
permdock rls generatesuggests indexes only for the columns its policies and helpers look rows up by: scope keys, condition fields compared with the subject, a claim or a membership, and membership user columns. It no longer suggests indexes for comparisons with constants such asstatus: 'sent', so theindexespart andrls verify --introspectwarnings lose those entries. With the new--db $DATABASE_URLflag,generatealso leaves out an index an existing index already leads with, and one whose table or column the database lacks, with a warning. -
permdock rls generatewritespermdock_can_assign_any(p_role, p_tenant, p_scope, p_scope_id)and, indatabasemode,permdock_can_assign_any_for(p_user, ...)whenever a role declaresassigns: one check for a declared or a custom role.permdock_can_assign_custom_roleis now written with custom roles indatabasemode whether or notrls.assignmentsis set. Thepermdock.manifest.jsonwritten bypermdock supabase inspectlists the_forhelpers and the assignment checks inrls.helpers, and addsrls.customRoles,rls.roles,rls.suspensionandrls.assignments, so a package writing SQL next to the helpers can call them without reading PermDock's config. Regenerate the manifest withpermdock supabase inspect --out. -
permdock rls generatewritespermdock_trusted_role_permissions(p_role, p_scope, p_tenant, p_scope_id)next to the member-facingpermdock_role_permissions: the same rows and argument checks without the caller check, for server code such as an admin backend, a support console or a job. No client role may execute it, and the newrls.trustedReaderslists the Postgres roles it is granted to, such as['service_role', 'support_reader']. -
permdock rls generatewritespermitted_<scope>_permission_keys(p_id)for each scope: every permission key the caller holds on one instance, aspermdock_has_permissionorpermitted_<scope>_ids_by_permissionwould answer each key, in one set-based query. A screen or RPC that checks fifteen permissions on an organization makes one call instead of fifteen. The overloadpermitted_<scope>_permission_keys(p_id, p_keys)answers for the listed keys only. Indatabasemode the_for(p_user, p_id)and_for(p_user, p_id, p_keys)forms answer for a named user and no client role may execute them. -
permdock rls verify --advisorsrunssupabase db advisors --type securityon--dbor the local stack, names the doctor check for each lint and exits 1 on aWARNorERRORrow. With--revoke-columns, the<table>_visible_fieldscompanion now lives in the helper schema (rls.schema) instead ofpublic, so Supabase'ssecurity_definer_viewlint no longer flags it; regenerate to move it. -
permdock rls verify --treeno longer seeds placeholders that break a column's constraints. A required column it fills gets the first label of its enum type, or a value its single-column check constraint admits (the first literal of an= 'x'orin (...)check, the bound of a> nor>= ncheck), before falling back to a placeholder of its type. The newrls.treeValuessets column values for every row seeded into a table, keyed by table name, for constraints the generator cannot read. -
permdock skills installfollows a symlinked agent or skill folder (.claude/skills/permdocklinked to.agents/skills/permdock) and writes the files once at the target, where it failed with "Cannot overwrite non-directory" before.permdock skills install --checkcompares the installed skills with the package version, writes nothing, and exits1listing every missing or changed file. -
permdock.derive({ customRoles?, approvalPolicies?, relations? })returns an instance for the same subject, active tenant, team, actor, delegation, sink and limits with the sources it names read again, so an app no longer rebuilds the instance fromcreatePermDockto add a tenant's approval policies or every custom role of a tenant. It returns a promise only when a named source is asynchronous; afromSnapshotinstance returns itself, andpermdock/pdpkeeps its providers. -
permdock.filter()no longer builds a decision token, freezes a decision or computes alternatives for each row it drops or keeps. Filtering 1,000 rows takes about a quarter of the time it did (pnpm bench). -
permdock.snapshot()is typed by overload: without asignerit returnsSnapshot, with onePromise<string>, and only an options value whosesignerthe compiler cannot see returns either. Callers drop theinstanceof Promisecheck or theparseSnapshotround trip they needed before. The options type is exported asSnapshotOptions. An instance fromfromSnapshotnow signs when given asigner, as the overloads say, instead of returning the plain snapshot. -
permdock/a2aandpermdock/mcpnow decide through the shared agent kernel. An A2A task failure'sstatusfollows the Problem Details matrix:401with awwwAuthenticatechallenge for an anonymous caller,429and503for limits,403otherwise.permdock/terminalprotectexits withEX_NOPERMwhenloadthrows or returns nothing for an instance permission.permdock/convexaccepts the instance options (memberships,sink,limitsand the rest).permdock/testingaddstestAgentAdapter. -
permdock/ai-sdktakesunmapped: 'deny' | 'allow'and returnscomposeToolApproval(app). With'allow', tools thetoolsmap does not bind to a permission stay incapabilityMiddleware's list and passtoolApproval; the default'deny'keeps hiding and denying them.composeToolApproval(app)runs PermDock's verdict first and asks the application's own approval function only for a call PermDock grants, so apps no longer wrap the adapter to compose a registry's confirmation. It returns the application's answer as is:undefinedleaves the decision to the tool's ownneedsApprovalor the SDK default, and an answer that is not a tool approval result denies. -
permdock/approvalsexportsstoredApprovalToken(store, decision, { denyPending?, now? }), the helper the adapters use to resume a call by its recomputed token, so an application that decides approvals in its own gate passes it toresumeDecisioninstead of reimplementing it. -
permdock/approvalsexports the list-paging helpers the built-in stores use, so a customApprovalStoreno longer copies them:pageApprovals(matching, query)pages requests filtered in memory,approvalPageSize,encodeApprovalCursoranddecodeApprovalCursor(with theApprovalCursorPositiontype) serve a store that pages in its own query, andlistAllApprovals(store, filter)followsnextto the last page. -
permdock/fastify: thepermdockHandlerdecision endpoint reuses the subject thepermdockplugin resolved for the request, sosubjectruns once per request, as it does in Express, Node and Nest. -
permdock/mcpaddscacheScope: 'private'to a filteredtools/list,prompts/list,resources/listorresources/templates/listresult only when the request was sent under protocol revision 2026-07-28 or later. A 2025-era result no longer carries the field, which that revision does not define. -
permdock/mcptakesactorKindoncreatePermDockandsubjectFromMcp, thekindof the actor built fromauthInfo.clientId(default'mcp-client'). Set it to'oauth-client'so an OAuth token is the same actor kind inpermdock/mcpas inpermdock/supabase,permdock/jwtandpermdock/a2a, and one policy delegation covers it on both surfaces. -
permdock/mcptools takeoauthScopes, the OAuth scopes that reach the tool in place of those derived from its permission (the permission's own scope and the coarse scopes ofoauthScopesin the policy). Any one of them reaches the tool and the first is the one aninsufficient_scopechallenge names. They only narrow: the token's delegation must still cover the permission, so the policy lists the permission under each coarse scope that may reach it and each tool names the one it needs. Two tools that share one permission can now need different coarse scopes, such as reading an export withmcp:readand creating one withmcp:write. Underenforce: 'procedure',oauthScopesFor(name)supplies them for the listing. An empty list throws at registration. Tools without the option behave as before. -
permdock/nestaddsPermDockModule.forRoot({ guard: 'global' }), which registersPermDockGuardasAPP_GUARD, anddecorateMethod(cls, key, ...decorators)for code without decorator syntax. Gateway messages run the same OAuth scope check, loader, decision and approval resume as HTTP routes, andPermDockExceptionFilterhandlesPermDockRevokedError.Protect(null)in Nest, andprotect(null)inpermdock/trpcandpermdock/orpc, need a principal and the declared OAuth scopes but no permission.permdock/expressanswers a PermDock error frompermdock(),protector the decision endpoint with Problem Details instead of passing it tonext(err).permdock()inpermdock/orpcreusescontext.permdockonly when it built that instance for the same tenant.permdock/convextakestenantandactoroptions, and itsConvexErrorcarries the marker Convex needs to senddatato the client.joseTokenVerifierkeys cached JWKS byjwks_uri, so keys from an old URI never answer after discovery moves it. -
permdock/nextandpermdock/next/clientnow load in plain Node ESM, such as Vitest withpermdockexternal or a Node script, instead of failing withERR_MODULE_NOT_FOUNDfornext/cache. The entries import Next.js modules by file and routenext/navigationthrough the package import#next/navigation, so Next.js still resolves its server build underreact-server. -
permdock/next:getPermissionlets Next.js interrupts (redirect(),notFound(), request-time bailouts) fromsubjectortenantpass through instead of turning them into a denial, and takes{ tenant }as a third argument. The serverPermDockProviderno longer replaces a failed snapshot with an empty one: hooks answerserver-only, and withsuspendthe error reaches the nearest error boundary. Instances with a factoryonDeniedare frozen, and theirtenant(),team()andderive()keep the handler. -
permdock/next: the factory returnsgetSnapshot({ tenant?, include?, tenants?, tags? }). Call it inside your own'use cache: private'function: it builds the session's snapshot and callscacheLife(cacheLifeFor(snapshot))andcacheTag(snapshotTag(sub), ...tags)in that scope.PermissionBoundarydeniedandapprovalalso accept a function of the boundary state. -
permdock/openapiexportsoperationPermissions(operations, { base? }), one declaration of each API operation's permission as"METHOD /path/{param}"to a permission (or{ permission, operationId }), withforRequest(method, path)for an HTTP gate andforOperation(id)forpermdock/mcp'spermissionFor, andoperationPermissionsFromOpenApi(document, permissions), which builds it from thex-permdock-permissionsa description carries. REST routes and the MCP tools over them no longer need separate route tables that can drift apart. -
permdock/react-nativeaddssecureStoreStorage(SecureStore), which splits values into 2048-byte chunks, and the synchronousmmkvStorage(mmkv). It also addsappStateForeground(AppState)andnetInfoOnline(NetInfo)for the provider'ssubscribeForegroundandsubscribeOnline.useSnapshotReady()is for the splash screen,usePermissionGuard(permission | permission[], data?)is forStack.ProtectedandTabs.Protected, and the type guardparseLocalSnapshotManifest(value)is also exported frompermdock. Asourceis read again on each return to the foreground. -
permdock/serverexportscreateServerKernelwith theServerKernelandServerKernelOptionstypes, the kernel every bundled HTTP adapter is built on.memoryMembershipSourceandmemorySnapshotSourcejoinmemoryRoleSourceas in-process defaults, andtestMembershipSourcetakes atenantto pass to everymembershipsFor. -
permdock/supabaseaddsparseSupabaseManifest, which reads apermdock.manifest.jsondocument (parsed, or the JSON text), validates it againstschemas/supabase-manifest-v1.jsonand returns a frozenSupabaseHookManifest. It throwsPermDockValidationErrorwith every issue, so a manifest of another major is refused.SupabaseManifestActiveRowis now exported. -
permdock/vue,permdock/svelteandpermdock/solidexportPermissionBoundary, which rendersdeniedorapprovalfor a thrownPermDockDeniedErrororPermDockApprovalRequiredError(Svelte 5.3 or later). Their providers acceptendpoint: false, read aheadersgetter on every request, and callrefresh({ tenant })when atenantref, getter or prop changes.useAssignablePermissionsandassignablePermissionstake a getter, Vue exportsProtectedProps, andpermdock/svelteexportsrequiredPlansunder thesveltecondition.Breaking: Vue composables and Solid hooks throw when called outside
setup(), aneffectScopeor a reactive owner, because their store subscription could never be removed there. -
permdock/vue,permdock/svelteandpermdock/solidre-run a check when a reactive row changes in place again; onlypermdock/reactreuses decisions within a render pass. In every UI adapter, a throwing subscriber no longer stops the others from seeing a logout. Approval polls also back off on errors and stop after a 4xx or five failures, and a snapshot for another user drops the previous user's approval state. -
permdockExtensionfrompermdock/prismanow scopesfindFirstOrThrowandgroupBy.verifyDpopProoffrompermdock/jwtcompareshtuwithout the request's query and fragment, per RFC 9449 section 4.3. A newreplayoption oncreateJwtSubjectResolverwithsender: "dpop"rejects a reused proofjti, andmemoryReplayStoreis exported frompermdock/jwt. -
permdock_can_assignnow answers for global roles. A global role whoseassignslists another global role may assign it, and the check takes a nullp_scope_idfor that assignment. A scoped role is never assignable with a null instance, by a global assigner or anyone else. Therls.assignmentstriggers treat a row whose instance column is null as a global assignment: a declared role goes throughpermdock_can_assign(role, null)and a custom role throughpermdock_can_assign_custom_role(null, 'global', null, role), the platform custom-role checks. One invitations table can now hold tenant and platform invitations. Regenerate the RLS output; a row with a null instance that names a scoped role is now refused. -
permittedIds(permdock, permission, scope, { within?, conditioned? })lists the instances ofscopein which the subject holdspermissionwith no row condition, minus the instances a deny reaches, within its delegation: the in-process mirror ofpermitted_<scope>_ids_by_permission. Apps no longer loop over memberships callingcanwith a made-up row to list, for example, the customers a portal contact may open. Both sides treat a validity window as a row condition and afieldslist as none. Withconditioned: true, and the SQL overloadpermitted_<scope>_ids_by_permission(p_permission, p_conditioned boolean)and its_forform, the list includes the instances a conditioned allow reaches and subtracts only unconditional denies, leaving the row condition to the caller.grant_keystakesp_effect'conditioned-allow'and'conditioned-deny'for the keys that carry a row condition. -
postgrestSourcesandsupabaseApprovalStorenow take their client asSupabaseRpcCaller, which a supabase-js client typed with a generatedDatabase(SupabaseClient<Database>) satisfies. Before, itsrpctyped the arguments asneverfor a function name theDatabasedid not declare, so the call needed a cast to an untyped client.SupabaseRpcClientstays the type to implement a fake against; it satisfiesSupabaseRpcCallertoo. -
postgrestSourcestakespolicy. With it,customRolesreads nothing while every role the subject holds is declared, andmemberships.versioncallssubject_forinstead ofauthz_version_forwhen the token claims a custom role, so a custom-role token costs one call instead of two and a current token with declared roles keeps the version-only call.MembershipSource.versionnow receives therolesandmembershipsthe token claims next toid. -
problemDetailsinpermdock/openapiis a Standard Schema, with a JSON Schema, for the Problem Details body of a denial. When an oRPC contract declaresoc.errors({ FORBIDDEN: { data: problemDetails } }),protectfrompermdock/orpcthrows through that constructor, so clients receive a defined, typed error. -
protectServer(server, { enforce: 'procedure', permissionFor })inpermdock/mcplists and annotates tools by permission but leaves the call to the procedure the tool runs, so each call decides once.permissionOf(procedure)inpermdock/orpcreturns the permission of a procedure'sprotect. -
resource({ restricted })takes{ field, stops }besides a column name.stopslists the inherited paths a restricted row closes:'parent'for the parent walk and link names for grants that cross those links.restricted: { field: 'restricted', stops: ['parent'] }hides a node from shares on the folders above it while everyone who reads its drive throughinherit(drive.read, { through: ['drive'] })still reads it and its subtree;stops: ['drive']closes only the drive link, and the parent walk then passes restricted rows. A closed link now also stays closed below a restricted row: a grant across it does not reach a row of a self-parented resource when a row above it within 32 parent hops is restricted, and denies withrelation-depthwhen the chain goes on past that.can,where()(Drizzle, Kysely andresolveRelated, with a closure mapping or a recursive walk) andpermdock rls generateagree; RLS reads the newpermdock_restricted_<resource>()helper, and the closure keeps 32 levels for such a resource.definePermissionsrejects an emptystops, an undeclared link and'parent'without aparent. The catalog carriesrestrictedStops, andRelatedConditiongainsrestrictedAncestorsandpassRestricted.Breaking: the string form still closes the parent walk and every link, and now applies the check below a restricted row too, so children of a restricted row are no longer reachable through a link such as the drive. Use
stops: ['parent']to keep a link open.memoryRelationskeeps walking past a restricted row when itsrestricteddoes not close'parent', flags the start row inrestrictedeither way, and setstruncatedonly when the next row exists. Regenerate the RLS SQL. -
resource(…, { levels })declares named conditions such asown,teamandall, and a custom-role grant picks one with{ permission, level }. The level is ANDed into the ceiling grant inresolveCustomRole, snapshots,customRoleClaim(key@level) andrls generatein both modes; an undeclared level is dropped asunknown-leveland denies.assignableLevels(permission)lists the levels a subject may hand out, the catalog listslevelsper permission,permdock diffreportslevel-removedas breaking, and a snapshot binds every principal attribute it does not carry, such asteamIds, sofromSnapshotagrees withdecide. -
resumeDecisiontakes attlin milliseconds for the request it opens, and a request opened byrequestApprovalor any adapter without one now stays open for the store'sttl(memoryApprovalStore({ ttl }), or the new optionalApprovalStore.ttl) instead of always one hour. A grant'sapproval.ttlstill caps the window, and attlthat is not a positive whole number of milliseconds throws aRangeError. -
rls generate --custom-rolesindatabasemode now works with membership sources (fromTable,fromJunction,rls.membershipSourcesorsupabase.hook.memberships) instead of failing with--custom-roles in database mode needs rls.memberships.scopes.<scope>. Eachpermitted_<scope>_idsunions a custom-role branch over the source rows, matchingcustom_role_permissionsandcustom_role_includeson the row's first-scope instance astenant_idand its id asscope_id, through the samepermdock_ceilingview as a membership table. -
rls generate --grants-outandsupabase hook generate --grants-outmove only whatsupabase db diffdrops into the grants file: schema privileges, function privileges (including the revokes on the hook's version and protection trigger functions, which no file carried before), the revoke on thepermdock_ceilingview and itssecurity_invokeroption.rls generate --grants-outworks with thehelperspart, and with thehookpart as well the file holds the helpers' statements first and the hook's after them. Table grants, such as theselectgrants tosupabase_auth_admin, and thepermdock_auth_admin_read_*policies stay in the hook and helpers parts, becausedb diffdropped them on the next diff when they lived only in a migration. Regenerate the parts and the grants file; a project that applied an earlier grants file keeps its privileges, and the nextdb difffinds nothing to change. -
rls generate --helpers-onlycombines with--fields views(rls.helpersOnlywithrls.fields: 'views'): it writes the field views next to the helpers, so a role at one scope whose grants listfields(a portal contact) reads masked columns through<table>_visiblewhile the row policies stay hand-written.--revoke-columnsis still refused with--helpers-only, because the table grants are the application's. Before,--helpers-onlyrefused--fields, and an application with hand-written policies needed security definer functions to return field-safe rows per scope. -
rls generate --seeds-outtakes a migrations directory (a path ending in/, or an existing directory). It compares the rows with the newest migration there that starts with-- permdock:seeds v1and writes a new<version>_permdock_seeds.sqlonly when they changed, so declarative-schema projects no longer rewrite an applied seeds migration in place.--checkfails while that newest seeds migration is missing or out of date. -
rls generate --shimswrappers answer permissions whose grants are split by condition. A wrapper used to pass the permission key to the helpers, which take grant keys, so a permission seeded asquote.read#1andquote.read#2answered nothing. Each wrapper now carries, per helper scope, the grant keys of every permission: it answers from the keys of the unconditional allows, minus the instances where the caller holds a deny key. A conditional allow answers nothing through a wrapper, since a wrapper cannot apply a row condition; before, a permission whose grants all shared one condition answered as if they had none. The wrappers are nowsecurity definer(stillsearch_path = '', reading no table), so a caller needsexecuteon the wrapper only, notusageon the helper schema. -
rls generateadds helpers that take a permission key instead of a grant key:permdock_has_permission(p_permission), onepermitted_<scope>_ids_by_permission(p_permission)per scope, andgrant_keys(p_permission, p_scope, p_effect), plus_for(p_user, p_permission)forms indatabasemode. They answer with the permission's unconditional allows minus any deny and map a former key to the current one, so hand-written SQL no longer spells positional#ngrant keys, which change when a role's grants change. A permission whose allows on a scope all carry a condition answers nothing there.rls generate --shimsno longer answers from a break-glass grant key, which only the break-glass read may use. -
rls generateindatabasemode addspermdock_has_for(p_user, p_grant)and onepermitted_<scope>_ids_for(p_user, p_grant)per scope: the answers ofpermdock_hasandpermitted_<scope>_idsfor a user the caller names, from the same tables with the same expiry and suspension filters and no active-tenant narrowing. They are for trusted SQL that acts for a stored user, such as a job, a trigger or an approval decided later, which otherwise had to rewriterequest.jwt.claimsto ask the helpers.executeis revoked frompublic,anonandauthenticated; grant it to a backend role yourself when it calls them directly. -
rls generateno longer invents a table for a resource thatrls.tablesdoes not name. Withrls.tablesset and--helpers-only, such a resource gets no index in theindexespart or theindex suggestionwarnings, andrls verify --introspectno longer warns about a missing index on it;generatenames the skipped resources once. Without--helpers-only, its policies still targetpublic.<resource>, andgeneratenow warns that they do. A config withoutrls.tableskeeps mapping every resource to the table of the same name. -
rls verify --format pgtapnow asserts each fixture against the database instead of writingselect ok(true, ...). The script createspg_temp.permdock_decision(statement text), seeds the fixture custom roles, and for each fixture binds the subject with the same settings as--db, runs the statement--dbruns and checksis(..., 'granted' | 'denied', ...)against the outcomecan()gave; an opaque grant becomesskip(). A fixture that disagrees with itsexpectedor names an unknown permission now exits1without a script.rls verify --dbkeeps custom-role rows that already exist (on conflict do nothing) instead of failing on them, and its success line readsin-process and against the databasewhen it checked the database. -
rls verify --treealso seeds distinct values in a column that a unique expression index reads, such asnamein(drive_id, coalesce(parent_id, ''), lower(name)). The unique columns came only from the index's plain key columns, so every sibling row got the same placeholder and the seed failed on such an index. -
rls verify --treeseeds a different value in each row for a required column that a unique index covers. Sibling rows got the same placeholder before, so a table with a unique index such as(drive_id, parent_id, name), or a unique uuid column, refused the seed and the command could not run. Text gets a numbered placeholder, a uuid a new value, a number counts up from its check's bound and a date moves a day per row; a column whose enum or equality check allows one value keeps it. -
rls.anonExecute: truegrantsanonusage on the helper schema andexecuteon every generated helper, for hand-written policies that apply topublicoranonand call them. Without it only field views thatanonreads get that grant, and a statementanonruns that reaches a helper fails, or as an InitPlan has crashed the backend on some Postgres builds. The docs now say that a policy calling a helper should sayto authenticated. -
rls.apiKeysholds a user key whose claim names atenantto that tenant. The generatedpermitted_<scope>_idsandmember_<scope>_idskeep only the key's tenant and the instances inside it, whateverrls.tenantssays, so withrls.tenants: 'all'a user in two tenants whose key names one reads only that one. A scopememberOfcheck compares its tenant column with the key's tenant, a scope outside the first scope's chain admits nothing for such a key, andpermdock_hasreturns false for it, since a global role reaches every tenant. A key without atenant, a tenant service key and the_forhelpers are unchanged. -
rls.apiKeyslets the database narrow an API key request the waysubjectFromApiKeydoes in process. A backend that verified a key passes it in a claim (api_keyby default, withscopes,tenantandroles), andpermdock rls generatewritespermdock_api_key_allows(p_grant):permdock_hasand everypermitted_<scope>_idsreturn nothing for a permission the key'sscopesdo not list, and allows that call no helper get the same check in their policy. Denies always apply, and a request without the claim is unchanged. A key with atenantand an empty or missingsubis a service principal of that tenant:permitted_<first scope>_idsandmember_<first scope>_idsanswer for it with the claim'sroles, orrls.apiKeys.serviceRoleswhen the claim lists none.claim,scopes,tenantandrolesrename the claim and its fields. The Supabase manifest lists the settings asrls.apiKeysand the helper inrls.helpers. Off by default; nothing changes without it. -
rls.approvals: truemakesrls generateadd an approval store to the helpers: anapproval_requeststable and onesecurity definerfunction perApprovalStoremethod (permdock_approval_open,_get,_resolve,_consume,_list,_expire,_cancel), each one atomic statement and closed to client roles.supabaseApprovalStore(client, { schema?, ttl?, onOpen? })inpermdock/supabaseimplementsApprovalStoreover them through supabase-js and passestestApprovalStore;onOpenruns after a request is stored, for notifications and superseding older requests.APPROVAL_POLICY_UNAVAILABLEis exported for the detail of a denial caused by a failingApprovalPolicySource. -
rls.approvalsaccepts{ table, token?, body?, mirror? }to put the generated approval store on a table the app already has.rls generateadds the token and body columns when missing, indexes them and writes the samepermdock_approval_*functions, sosupabaseApprovalStoreworks unchanged. The functions read every field from the body, skip the app's rows that have none, and copy the fieldsmirrornames into the app's columns on each write, converted to their types. The table's grants and policies stay the app's. -
rls.approvalson an adopted table takesopen: 'attach'andschema. Withopen: 'attach',permdock_approval_openattaches the request to the row the app inserted with the decision's token, so tables with required columns only the app can fill work withsupabaseApprovalStore; with no such row it raisesP0002and the open fails.schemawrites the store functions into another schema, such aspublicwhen the helper schema is not exposed, sosupabaseApprovalStore(client, { schema })reaches them without wrapper functions. -
rls.assignmentsadds assignment triggers indatabasemode: each scope'srls.membershipstable, and each table inrls.assignments.tablessuch as invitations, gets abefore insert or update or deletetrigger that refuses a client role's write of a role the caller may not assign at that instance (SQLSTATE42501, hintnot-assignable-by). Declared roles follow theassignsgraph throughpermdock_can_assign; custom roles go through the newpermdock_can_assign_custom_role, which runs the custom-role write checks on the stored definition. Writes that do not run as a client role (the owner, asecurity definerfunction, a backend role) are trusted, which covers bootstrap paths without a bypass setting. -
rls.assignmentsnow guards the global-roles table. Whenrls.roles(elsesupabase.hook.roles) names the application's table,rls generateputs apermdock_assignmenttrigger on it too: a client role may insert, change or delete only the global roles it may assign, checked like a tenant assignment at no instance throughpermdock_can_assign(role, null)and, for a platform custom role,permdock_can_assign_custom_role(null, 'global', null, role). A client that holds no global role whoseassignslists the role can no longer give itself or anyone else a global role through the Data API. The generateduser_rolestable is unchanged, since no client role may write it. The hook manifest lists the table inrls.assignments.tables.rls.assignments.ownRole: 'refuse'refuses a client write to a row whose user is the caller, on every guarded table with a user column, whatever the role; a map such as{ 'public.user_roles': 'refuse' }limits it to the tables it names. A listed table names its user column with the newuserfield. The refusal raises SQLSTATE42501with hintself-demotionfor the old row andnot-assignable-byfor the new one.permdock doctorPD064 warns on a tablerls.assignmentsguards that no migration orrls.outfile creates thepermdock_assignmenttrigger on.An application that sets both
rls.assignmentsandrls.rolesand lets clients write its global-roles table directly gets those writes checked after regenerating; move such writes into asecurity definerfunction or a backend role, which stay trusted. -
rls.customRoleWrites.requiresmakes the generated custom-role write functions check that the caller may manage roles before any hand-out check: it names permission keys, or'manageRoles'for every permission withmeta.manageRoles. A caller holding none of them in the tenant, or through a global role, gets42501with hintmanage-rolesfrompermdock_replace_custom_role_grants,permdock_rename_custom_role_grantsandpermdock_delete_custom_role_grants. -
rls.customRoleWrites.rolesnames the application's own table of custom roles (table,key,tenant, and optionallyscope,idand askipcolumn for declared or system rows).rls generatethen addspermdock_cascade_custom_role()and anafter update or deletetrigger on that table: a rename or a move to another tenant, scope or instance carries the role's grants and includes, and a delete removes them. A signed-in caller passes the same checks as the write functions where the role was and where it lands; migrations, jobs and nested triggers move the rows directly. -
rls.jsonSchema: true | 'auto'adds a pg_jsonschema check constraint that each approval storebodymatches the newschemas/approval-request-v1.json.supabase.hook.validate: truemakes the token hook check its claims againstsupabase-claims-v1.jsonand drop them all on a mismatch instead of failing sign-in. -
rls.ownershipTriggers: falseleaves out the holder-count and transfer-only triggers thatmin,maxandtransferOnlyput on the membership tables, andrls.ownershipTriggers: { <scope>: false }leaves them out for the named scopes, for an application that enforces those counts its own way.decideRoleChangekeeps checking the rules andpermdock_can_assignis still written. -
rls.readOnlyActorsadds a restrictive policy per table and write command that refuses writes from support and impersonation sessions (the token'sactclaim) unlessact.read_onlyisfalse, replacing the hand-written policy the support access guide showed. -
rls.realtimeandrls.storage(andsupabaseRls({ realtime, storage })) makepermdock rls generatewrite policies onrealtime.messagesfor private channels and onstorage.objectsfor buckets. A topic segment or a folder of the object name selects the scope instance, and the policies callpermitted_<scope>_ids_by_permission('<key>'), so they count only the allows without a row condition, minus any deny, and a permission split into#ngrant keys still matches. For a permission that also has a relationship grant or awhere,rls generatewarns that its topic or folder admits only the instances its role allows reach. Doctor check PD037,rls verify --dbandrls verify --introspectno longer flag a policy that calls the permission-key forms, which never grant more than the application does, and the PD037 fix names them. -
rls.rowHelpers: true(or a list of resources) makespermdock rls generatewritepermitted_<resource>_rows(p_permission): the ids of the resource's rows the caller may act on with one permission, from the same allows and denies the generated policies use, so closure walks, link hops, the restricted stop,includes, groups andrequiresapply without being restated in hand-written policies and functions. Actions with no SQL command get an arm too.permitted_<resource>_rows_for(p_user, p_permission, p_claims)answers for a stored user and is executable by no client role; theneondialect gets no_forform. It works withrls.helpersOnly.permitted_<resource>_row(p_row, p_permission)decides one row value from its columns instead of looking it up by id. A table's ownselectpolicy written asusing (permdock.permitted_doc_row(doc, 'doc.read'))admits a row the same statement inserts, soinsert ... returningpasses row-level security. -
rls.tablesaccepts schema-qualified names such asintegrations.webhook_endpointsend to end.permdock rls verify(with--dband in the pgTAP output), its field view reads andrlsParityfrompermdock/testingnow quoteschema.tableas a schema and a table instead of refusing it as an unsafe identifier, so a table a package installs in its own schema can be verified like one inpublic. With--target drizzleor--target prisma, the default export or model name of a schema-qualified table is the table name without its schema;rls.drizzle.exportsandrls.prisma.modelsstill override it. Nothing changes for unqualified names. -
rls.tenants: 'all'stops the generated RLS helpers from narrowing to the tenant claim:permitted_<scope>_ids, the rootmemberOf(nowin (select member_<scope>_ids())) and the nested membershipexistsadmit every tenant the subject is a member of, for apps whose tenant comes from the URL. The default,'active', keeps the current behaviour: narrow to the tenant claim when the token carries one. -
rls.trustedReadersacceptsservice_role.rls generatethrew "generated RLS must never emit service_role" when the list named it, althoughservice_roleis the trusted server role in a Supabase app. The execute grant onpermdock_trusted_role_permissionsmay now name it, and the generator still refusesservice_roleon every other line. -
securityFor(permission)andpermissionsExtension(permissions)inpermdock/openapireturn an operation'ssecurityandx-permdock-permissionsfrom permission references alone, so a contract package can write them without importing the policy.protectfrompermdock/orpcnow attaches to contract procedures that declare anoutput. -
subjectFromSupabaseandactorOfinpermdock/supabaseread a supportactlevel withoutread_onlyas read-only (Actor.readOnly: true), as better-supabase does. Before, such a token could reach every delegated write. -
subjectFromSupabaseanddelegationOfleave the OpenID Connect identity scopes (openid,profile,email,address,phone,offline_access) out ofdelegation.scopes. A Supabase OAuth server token that carries only those scopes is now anoauth-clientactor with no delegation: it is still denied every check withno-delegationby default, and a policydelegationsentry that names the client now lets it act within that ceiling. Before, the identity scopes counted as a token delegation that covered nothing, so the client was denied withnot-delegatedeven when the policy delegated to it. -
supabase.hook.beforenames schema-qualified functions(event jsonb) returns jsonbthat the generatedcustom_access_token_hookcalls first, in order. A result with anerrorkey is returned as the hook's answer, so Supabase Auth refuses the token, and anullor non-object result is refused with status 500; otherwise the hook continues with the event the function returned and writes its own claims as before. The migration grantssupabase_auth_adminusageon each function's schema andexecuteon the function, and the Supabase manifest lists them ashook.before. Use it for checks such as single sign-on enforcement that must run inside the one hook Supabase allows. Nothing changes without it. -
supabaseApprovalStore'sonOpennow runs once per stored request. A repeated ask whose token finds the request still open keeps the stored request, as before, but no longer runsonOpenagain, so an app that notifies approvers or opens its own record there does not need to make it idempotent. An open whose stored request cannot be read back runs noonOpeneither. -
usePermission,usePermissionsanduseApprovalfrompermdock/reactandpermdock/react-nativenow update after a snapshot change when the React Compiler compiles them. Before, a compiled hook kept its first answer, such aspending. -
vouchApproval(store, token, { status, by, rule, note? })inpermdock/approvalsrecords a verdict that the application's own approval rules decided, such as a manager chain, delegates or a quorum it counts itself. The store resolves the request at once and recordsvouched: ruleon the request and on the approval, so the audit trail says which rule decided. The eligibility checks ofapprovers, the tenant membership and the quorum are skipped; the actor, the principal (unlessdistinct: false), a repeated approver, an unauthenticated subject and a closed or expired request are still refused.ApprovalVerdict.vouchedis server-side input thatapprovalsHandlernever reads from a body.applyApprovalVerdicthandles it, so the memory store, the generated Supabase store and custom stores built on it support it, andtestApprovalStorechecks it. The approval request wire format (v: 1) gains the optionalvouchedfield.
0.1.0 (2026-10-04)
-
A
401to a request without anAuthorizationheader now carries a bareBearerchallenge instead oferror="invalid_token"(RFC 6750 section 3.1), and a rejected token gets a fixederror_description. The/unauthenticatedbody no longer carriesdenialsoralternatives, so nothing explains the failure to an unauthenticated caller.problemFromDecisiontakes acredentialsoption for the challenge. -
A deny that cannot be evaluated now denies the decision, as a relation deny already did: a deny closure that throws or returns a thenable (
closure-error) and a deny with an opaque condition (opaque-condition). An opaque node nested undernot,ororandnow fails its grant withopaque-conditionon the server and infromSnapshot, sonot(opaque)no longer matches.coveredByDelegationtreats an RFC 9396 or GNAP entry whoseidentifieris not a string or whoseactionsis not an array as covering nothing, instead of ignoring the field. -
A dotted condition field, relation or identifier with a
__proto__,constructororprototypesegment (a.__proto__) is now rejected at definition time like the bare key, as the threat model states.permdock/testingrenames theClientStoreDocktype toClientStorePermDock. -
A grant whose roles the subject holds but whose
plangrantee it lacks now denies with the new reasonnot-entitledinstead ofno-grant, with the grantee into; a plan grant whose roles are not held adds no denial. When every denial isnot-entitled, HTTP adapters answer403/not-entitledwithplans(the plan keys that would grant),describe(decision)returnskind: 'upgrade'withplans, and the newrequiredPlans(decision)returns the list. Snapshots carry these grants asnotEntitledentries, which grant nothing, sousePermissionin every UI adapter gets the same reason and plans asdecideon the server. -
API keys end in a 6-character base62 CRC-32 checksum:
pdk_<id>_<secret><checksum>.generateApiKeyappends it andparseApiKeyrejects a key whose checksum does not match, so secret scanners can matchpdk_[A-Za-z0-9_-]{1,128}_[A-Za-z0-9]{49}and discard lookalikes offline; keys generated before this change no longer parse.CredentialVerifiergains an optionaltouch(id, at)thatsubjectFromApiKeycalls, without awaiting, after a key resolves, for alastUsedAtcolumn;apiKeyVerifier({ find, touch })passes it through,memoryCredentials()records it (lastUsedAt(id)), andtestCredentialVerifierchecks that a touch never changes what a key verifies to. -
Add
toCsvRow(event)andCSV_COLUMNS: the pinned CSV export row for decision and approval events, shared by the Cloud export and self-hosted sinks. -
Approval grants take
quorum,ttlandescalation.quorum(default 1) is the number of distinct approvers a request needs:ApprovalStore.resolveappends each approval to the request's newapprovals: { by, at }[], keeps the requestpendinguntil the quorum is met, and refuses a repeat approver with the newapprover-repeatedcode (409fromapprovalsHandler); one rejection ends the request.ttl(a duration such as'30m') caps the store's window whenrequestApprovalsetsexpiresAt.escalation: { after, to }letstoapprove onceafterhas passed sincecreatedAt. The request'sapproverscarriesquorumandescalation;applyApprovalVerdict,approvalQuorumandescalationOpenAtare exported frompermdock/approvalsfor custom stores, and the Drizzle recipe uses them.definePolicyrejects a quorum below 1, a malformed duration or a relation inescalation.to; hosted grants with a smaller quorum or a longer ttl than a code allow are dropped asweaker-approval. The catalogapprovalcarries the three fields, sopermdock diffreports a change to any of them. -
Approval tokens bind the call's data when there is no row id: for a collection action, or a row without its
idfield,Decision.tokenincludes a SHA-256 of the canonical JSON of the validated data, so an approved call no longer covers a retry with different arguments.approval.byrefusesrelation()atdefinePolicy, and an approval store refuses every approver for a stored request whose approvers include a relation.permdock/terminalprompts y/N only for a grant withapproval: { distinct: false }and no actor; any other approval is recorded in thestoreand the command exits75until someone else approves it, or exits77when nostoreis configured.storedApprovalTokenmoved from the agent kernel to the approvals helpers. -
Approvals can go stale when the row changes.
resource(schema, { version: 'updatedAt' })names the row field that changes with the row, andapproval: { staleOn: 'resource-change' }hashes its value into the decision token (the prefix stayspd1.; tokens of grants withoutstaleOnare unchanged). Resuming with a token that approved an earlier version of the same row denies with the new reasonstale-approvaland leaves the old record unconsumed; the next call without it is a newapproval-required.definePolicythrows forstaleOnon a collection action or on a resource withoutversion. Approval requests carryapprovers.staleOn, the catalog carries resourceversionandstaleOninapprovals, and a hosted approval withoutstaleOnisweaker-approvalagainst a code allow that sets it. -
Approvals refuse the requester by default.
approval: 'human'andapproval: { by }now refuse the request's principal as approver as well as its actor (approver-is-principal), inassertApprover,ApprovalStore.resolve,resolveApprovalandapprovalsHandler. A grant that wants the user to confirm their own agent's call opts out withapproval: { distinct: false }, andpermdock doctorPD024 (--only self-approval) warns on each opt-out.requireDistinctApproverstays as a handler-wide floor that refuses the principal even on an opted-out grant. A hosted grant that setsdistinct: falseon a permission a code allow guards with an approval is dropped asweaker-approval.testApprovalStorechecks that a custom store refuses the principal on every approval shape and accepts it only withdistinct: false.Behaviour change: an approval request without
approvers.distinctnow refuses its principal. Setdistinct: falseon the grant to keep the old behaviour. -
Approvals take
mode: 'all' | 'sequential'withstages: [{ by, quorum? }],user(id)approvers, andrelation()approvers checked at verdict time throughapprovalsHandler's newrelationsoption (approverRelationsinpermdock/approvals). The approval request carriesmodeandstages, each signature itsstage, and the verdictrelations.createPermDocktakesapprovalPolicies, anApprovalPolicySource(memoryApprovalPolicies) whose entries add approval stages to matching allows and deny withapproval-policy-unavailablewhen they fail to load;testApprovalPolicySourcechecks a source. -
Arazzo
simulateandpermdock arazzo checkfollow the 1.0 and 1.1 specs more closely:- Documents are validated against the required fields.
- Patch versions are accepted.
$sourceDescriptionsforms ofoperationIdandoperationPathselect their source.$inputs.*and$components.parametersreferences resolve.- Nested workflows take their calling step's parameters as inputs.
- Results are kept per step position, so equal stepIds in nested workflows no longer collide.
simulatenow emits the onesimulateaudit event its docs describe, with the batch's worst decision and counts. -
Assign authority now comes from live, held memberships. Expired memberships no longer count toward
tenants(), the active tenant,assignableRoles()or a credential's ceiling.decideRoleChangereads a nested change's tenant from the subject's membership and denies a disagreeingwithinwithno-membership; pass{ trusted: true }when the application loadedwithinfrom its own store.activatecopieswithinfrom the eligible membership, and the approval token covers it. -
Bound the work one request can cause.
canandwhoCanwalk each nested group once per depth budget, so a lattice of shared teams costs time in its group count rather than its path count.createEvaluationsHandler(andpermdockHandler) answers a batch overmaxEvaluations(256 by default, the AuthZEN limit) with a 413 Problem Details before resolving the subject. A denial'salternativespeek a quota grant'sremainingwithout spending it, andcanskips alternatives entirely. -
CLI fixes found by the coverage suite:
- The generated barrel imports the permissions module relative to its own folder; before, the default
src/permissions.generated.tsimported./src/permissions.js, which does not resolve. collect,usageanddoctorskip a broken symlink in a source folder instead of crashing.permdock usageexits2with "policy export is not a Policy" when the policy module exports something else, instead of throwing (which crasheddoctorat PD003).- A computed identifier key such as
defineRoles({ [name]: … })no longer counts as a role namedname, which madeusagereport a false "granted by no role". - PD009 names the duplicate
permdockpackage folders, not their parentnode_modules.
- The generated barrel imports the permissions module relative to its own folder; before, the default
-
Catalog permissions carry
rowConditionswhenpermdock.config.tsnames a policy:truewhen a code grant for the key has awhere,check, closure, field list, purpose, break-glass override, or a relation, plan, actor or assurance grantee, which the SQL helpers (permdock_has,permitted_<scope>_ids) do not check.permdock doctorPD037 errors on a migration'sstorage.objectsorrealtime.messagespolicy that calls a helper for such a key, andpermdock rls verify --dbreports the same frompg_policies, so a Storage or Realtime policy written outside PermDock (better-supabasepermdockmode) cannot grant more than the application does. -
Catalog v1 carries a top-level
fingerprintand per-permissionapprovals.catalogFingerprint(catalog)is exported frompermdock: base64url SHA-256 over canonical JSON withoutgeneratedAt,generator,fingerprintandusages, so the CLI version and call sites no longer change it.permdock collect --checkignoresgenerator. -
Composed membership sources and
permdock supabase hook generate.membershipson everycreatePermDockaccepts an array of sources, merged and de-duplicated bycomposeMemberships, andMembershipSourcegains optionallist({ scope, id })(a scope's members),version(principal)andclaimsFirst.claimsFirst(sources, { version })keeps the verified token's memberships and reads the sources only when the token was truncated. Permissions listed indefinePolicy({ fresh })deny with the new reasonstale-credentialswhen token memberships are behind the source version; a stale subject's snapshot drops those allows.permdock/supabaseadds the SQL sourcesfromTableandfromJunction(fixed roles,via,within, expiry,managedBy, seats, suspension),authzVersion,supabaseMembershipsBudget, andsubjectFromSupabasereadsmemberships_truncatedandauthz_verintoprincipal.membershipsTruncatedandprincipal.authzVersion.permdock supabase hook generatecompiles the same sources intocustom_access_token_hook:user_roleandroles, a union over every source with the active scope first (--active-from), anattrsclaim from allow-listed server-owned columns andapp_metadata.<key>entries for attribute conditions (user_metadatais refused, and the migration refuses to install while clients can write a listed column), a byte budget overattrsandmemberships(--budget, default 1024) withmemberships_truncated,authz_verwith its version table and triggers, a trigger that refuses client writes to IdP-owned rows, thesupabase_auth_admingrants and revokes, and the printedconfig.tomlblock withjwt_expiry = 900.Memberships carry
managedBy: 'idp'(set bydirectoryMembershipSource;decideRoleChangerefuses with the new reasonexternally-managed,isExternallyManagedtells a UI) andentitlements(seats thatplan()grantees match inside the active tenant).EntitlementSource(entitlementsoption,memoryEntitlementSource,testEntitlementSource) adds plan names for the active tenant, andfromStripeEntitlementsreads Stripe Entitlements through a structural client.permdock doctorPD028 (--only attrs) flagsattrsentries a user could set. -
Custom roles in generated RLS.
permdock rls generate --custom-roles(orrls.customRoles: true) makespermitted_tenant_idsandpermitted_team_idsresolve tenant-defined custom roles as well: from the newcustom_role_permissionsandcustom_role_includestables indatabasemode, or from a compactmemberships[].grantsclaim injwtmode (off unless the flag is set). Both go throughpermdock_custom_keysand a generatedpermdock_ceilingview of the assignable declared roles, so a row or claim entry outside the ceiling never widens access, and the database agrees withresolveCustomRole.authorizeSql({ customRoles: { declared } })answers tenant requests from custom roles, and--rbac supabase --custom-rolespasses it through.customRoleClaim(roles)builds the claim map for a token hook.rls verifyfixture files acceptcustomRoles(resolved in-process, sent in the claim, and seeded into the tables indatabasemode), andrlsParityacceptscustomRoles. An included role's denies now apply to a custom role only in the custom role's own scope, matching what RLS can evaluate. Output without--custom-rolesis unchanged. -
Custom roles with
scope: 'global'are platform roles held throughprincipal.roles, capped by the allows of assignable global roles and returned by the newRoleSource.globalRoles(). Withrls generate --custom-rolesthey are rows with a nulltenant_id, or the top-levelrole_grantsclaim injwtmode, andpermdock_hasanswers from them. The--rbac supabasescaffold typesuser_roles.roleastextwhen--custom-rolesis on. -
Decision data is untrusted unless the caller marks it. With
validate: 'boundary',decide,can,filter,protect,conn.checkand the Nest guard validate a row against the resource schema unless the call passestrusted: true; pass it for rows your server loaded itself. The agent kernel labels tool databoundary: 'tool-args'. The AuthZEN handler forwards the loader'strustedflag and answersdecision: false(no-grant,detail: 'resource-unavailable') when the resource loader throws, instead of evaluating the request's{ id }stub. -
Denials that leave the process are now
WireDenial({ role, reason, to?, detail? }), andDecisionEvent.denialsandProblemDetails.denialsare typed that way. This covers decision events, Problem Details bodies, MCP and WebMCP refusals and the decision endpoint.detailis kept only as a JSON value: what a closure threw (closure-error), a validation error and anyErroror non-JSON detail are dropped, so they no longer reach a sink or a response body.WireDenialandWireDecisionare exported frompermdock. -
Doctor PD028 now also warns when the migrations let
anonorauthenticatedinsert or update a column that decides a membership: the user, scope, id,within, role,viaand expiry columns of everyfromTable/fromJunctionsource. A contact who can edit their own row could otherwise setuser_idorcustomer_idand give themselves a membership. The grant model matches Postgres, where a column-levelrevokeleaves a table-level grant in place.MembershipSqlgainscolumns, the list the check reads. -
Document that scope and tenant ids compare as exact text in
decideand the SQL helpers, never lower-cased, and that producers emituuid::text; the policy matrix and an RLS parity suite over auuidcolumn cover it. -
Document the PermDock Cloud contract v1:
client,adminandexportkey kinds, the authoring routes (/hosted-grants,/directory/*,/connectors,/export), the environment's JWKS and OIDC Discovery, RFC 8693 token exchange at<env URL>/oauth/tokenminting RFC 9068 access tokens, the raw sink body the Cloud envelopes as CloudEvents, andmembershipevents withsource: 'cloud'. -
Drizzle
toWherereturnsSQL, andcolumnsaccepts only columns of the given table.permdock/prismaaddsprismaModelFields, which reads required and list fields fromschema.prismaor a DMMF datamodel for the newmodeloption, andtoPredicate, which compiles a condition to a Prisma 8 field-proxy predicate. -
Edge fixes found by the coverage suite:
- SCIM list paging starts at the first item for a negative or non-finite cursor; before, it sliced from the end and returned a negative
startIndex. - An SSF back-channel logout token with an empty
suband asidmaps to an opaque session subject, not aniss_subsubject with an emptysub. PermDockCloudEventincludesdev.permdock.credential, whichverifyWebhookalready accepted.
- SCIM list paging starts at the first item for a negative or non-finite cursor; before, it sliced from the end and returned a negative
-
Elevated access: just-in-time role activation, break-glass and support access with tenant consent. Memberships gain
grantedBy(the principal id that wrote the row) andreason(free text); both round-trip through the Supabase custom access token hook claim, the snapshot and every event.Membershipalso gainseligible(roles a holder may activate but does not hold) andmember: { group }(a subgroup inside the instance).Role activation (E1).
role('admin', ADMIN, { on: 'organization', activation: { maxDuration: '4h', justification: 'required', approval: { by: roles.owner }, assurance: { maxAge: 300 } } })makes a role eligible-only: it is never held directly, a membership lists it undereligible, andpermdock.activate({ role, scope, id, duration, reason })returns aDecision. It isapproval-requiredwhenactivation.approvalis set; once granted it carries the membership to write underelevation(via: 'elevated', withexpiresAt,grantedByandreason). PermDock never writes the membership; the app does. Expiry uses the existingexpiresAtcheck plus C3's revocation counter, and the Supabase hook already includes elevated rows.permdock doctorPD033 warns on anactivationwithoutmaxDurationand on an activation role held standing in the memberships fixture.Break-glass (E2).
breakGlass(permissions.patient.read, { overrides: ['restricted-record'], requires: { purpose: ['BTG','ETREAT'], reason: true, assurance: { maxAge: 60 } }, maxDuration: '1h', obligations: ['notify','review'] })is the only deny override: it overrides deny grants whosenameit lists and nothing else, so deny-overrides-allow holds everywhere else. A satisfied break-glass grant isgrantedwithmatched.breakGlass: trueandobligations: [{ kind: 'notify' }, { kind: 'review' }, { kind: 'justify', reason }]; a break-glass grant is engaged bycontext.purpose, and a missing purpose, reason or assurance denies with the new reasonspurpose,reason-requiredorinsufficient-user-authentication.DecisionEventrecordspurposeandreason, andtoOcsfraises break-glass to high severity. RLS never compiles a break-glass grant (it stays non-portable); insteadpermdock rls generateemits apermdock_break_glass_audittable and onesecurity definerpermdock_break_glass_<resource>(permission)per targeted resource that checks a signed break-glass session, writes an audit row and returns the rows (bypassing RLS as the definer), so a plain RLS read keeps denying the restricted rows.permdock doctorPD034 flags a break-glass grant under anrlsconfig.Support access (E3).
supportAccess({ role: 'support', actorRequired: true, consent: { by: roles.owner, durations: ['1d','7d','30d'] }, forbid: [permissions.billing, permissions.security] })is a role a vendor holds only through a consented, time-boundvia: 'support'membership. A tenant owner consents through anApprovalRequestwhosemembershipis{ scope, id, via: 'support', roles: ['support'], member: { group: 'vendor-support' }, expiresAt, grantedBy }; group members ride C3'sfromJunctiongroupoption. WithactorRequired: true, every decision under a support membership denies with the new reasonactor-requiredunless the subject carries anact.forbidcompiles to deny grants scoped tovia: 'support'. The lifecycle emits the newaccess.started,access.endedandaccess.revokedevents (accessEvent), each a CloudEvents type (dev.permdock.access.started/.ended/.revoked) with an OCSF Account Change mapping (accessToOcsf); revoking consent bumps C3's revocation counter.permdock doctorPD035 warns on a support role withoutactorRequired.Purpose of use.
context.purposeis a decision input:allow(p, { purpose: ['treatment'] })applies only when the caller asserts a matching purpose. It is not portable; RLS honours it only through the break-glass session function.The catalog gains role
activationandsupportAccessentries and break-glass entries per permission. -
Every
DecideOptionsfield has JSDoc, so editors describetrusted,boundary,now,source,adapter,onDeniedandfieldon hover. The same text is the options table on the decisions page. -
Export
coveredByDelegation(permission, delegation, resourceId?, hasActor?)frompermdock: the delegation coverage checkdecideuses for OAuthscopes, RFC 9396authorizationDetailsand GNAPaccess. It returnsundefinedwhen covered, otherwiseno-delegationornot-delegated, andpermissionneeds onlyscope,resourceandaction, so an external PDP such as the PermDock Cloud AuthZEN endpoint can reuse it. -
Fail-closed fixes for a grantee kind this build does not know, as a forged snapshot or a newer policy document could carry:
- An allow naming one matched every caller, anonymous included; it now matches nobody, on the server, in
fromSnapshotand for approvers. - A deny naming one now applies to everyone.
- A hosted grant naming one is dropped as
unknown-grantee, and an approverbynaming one is invalid. whoCanreportscomplete: falsewhen it meets one.toOcsfmaps an outcome this build does not know tostatus_id0Unknowninstead of leavingstatus_idandstatusundefined.
- An allow naming one matched every caller, anonymous included; it now matches nobody, on the server, in
-
Fail-closed fixes found by the coverage suite:
subjectFromJwtwithactor: { kind }and nofromnow reads the RFC 8693actclaim. Before, a delegated token under that config resolved as the user acting directly, with no actor and so no delegation check.- A custom
verifier.verifythat throws insubjectFromJwt,subjectFromCapabilityorsubjectFromCiOidcresolves the anonymous subject with causemalformedinstead of rejecting. permdock/pdp: aprovider.decidethat throws or rejects isdeniedwithpdp-unavailable, and apermittedcallback that throws makesfilterandwherekeep no rows.
-
Field views.
permdock rls generate --fields views(rls.fields: 'views') emits, after the policies, onesecurity_invokerview<table>_visibleper table whose read grants setfields: a column some read allow leaves out or some read deny lists iscase when <permitted> then col end, where<permitted>iscan(permission, row, { field })built from the row policy's own per-statement helper calls (permitted_<scope>_ids,permdock_has) and row conditions; every other column and the row key pass through. In this mode grant keys also split by field set, and field-only read grants (a deny withfields, an allow with an empty list) leave the row policy and live in the masks, as they never decide the row incan. Whenanonreads a view, the helpers are executable byanontoo; they return nothing without a subject. The resource schema must expose Standard JSON Schema so the view can list its columns.--revoke-columns(rls.revokeColumns: true) closes the table:anonandauthenticatedgetselecton only the unrestricted columns and the key, and the restricted columns are read through<table>_visible_fields, asecurity_barriercompanion that runs as its owner, masks every column with the full field decision and keeps only rows with a readable column, left-joined onpermdock_key. It breaksselect *andreturning *on the table. Drizzle and Prisma targets put the views and column statements in the migration comment.permdock rls verifywithrls.fields: 'views'andrlsParity({ fieldViews: true })compare each read fixture's row in<table>_visiblewith the columnspickkeeps (reported asfields: { app, database }, typeRlsFieldsOutcome), and read back only the key so a closed table does not reject them;rls verifystatements now alwaysreturning "id".permdock rls importreads the generated views back as afieldViewsexport of per-column grants, from a dump or with--db.permdock doctorPD030 (--only fields) warns on field-limited columns a table still returns, and PD022 exempts the marked companion. -
First release of PermDock: typed, portable permissions for apps, APIs and agents, with the
permdockcore and adapters, thepermdockCLI and thepermdock/testingrunners. -
Generated RLS keeps parity with
decide.containsescapes\,%and_beforeLIKE.rls.suspensionnow also applies to the inline membershipexists, the root-scope tenant-claim check and the graph helpers.rls migratecounts only allow seeds when it checks that a key is granted on a scope. -
Global roles can be read by key through a roles table:
supabase.hook.roles.roleand the newrls.rolesaccept{ through: 'roles', on: { role_id: 'id' }, column: 'key' }. It ships because CentraKit, like most apps that manage roles in a UI, keepsuser_roles (user_id, role_id references roles(id)), and copying keys intouser_roleswould turn every rename into a data migration.permdock_has,permdock_can_assignand the token hook join through the table, the hook grantssupabase_auth_admina read on it, andpermdock_bump_authz_version_role_keysbumps every holder'sauthz_verwhen a key changes.rls.rolesdefaults tosupabase.hook.rolesand the hook's roles torls.roles; with it set,rls generateno longer createsuser_roles, and--rbac supabaserefuses it. -
Grants take
validFromandvalidUntil(RFC 3339 or Unix seconds), normalised toGrant.validity{ from?, until? }. Outside the window an allow denies with the newinactive-grantreason anddetail: { from, until }; a deny does not apply andexplainrecords it as skipped withwhy: 'validity'.where()andfilterdrop inactive grants, snapshot grants carryvalidityfor the client evaluator,permdock rls generateANDsnow() >= to_timestamp(from) and now() < to_timestamp(until)into the access check, andsimulate(checks, { now })previews a batch as of one instant. The window is part of the policy fingerprint, and the catalog marks such a permissionrowConditions: true. -
Graph grants reach the ORMs.
toWhereinpermdock/drizzleandpermdock/kyselycompiles arelatednode to one subquery when givenrelations: { tables?, closure? }, andresolveRelatedinpermdock/prismareplaces each node with the ids it reads through a raw query. All three adapters exportcheckRow, which tells a missing row from a denied one in one query, andwithSubject, which runs a transaction with the role and claims the generated RLS policies read. -
HTTP adapters answer an exhausted
limitwith429,Retry-Afterand theRateLimit/RateLimit-Policyfields ofdraft-ietf-httpapi-ratelimit-headers-11, andlimit-unavailablewith503; tRPC and oRPC map them toTOO_MANY_REQUESTSandSERVICE_UNAVAILABLE. Alimitdenial now carriesdetail: { count, window, resetsAt }(LimitDetail).resource()takesdisclosure: 'hide', which makes a denied row answer the same404/not-foundproblem a missing row gets; a missing row is now that problem instead of an empty404. A step-up challenge namesacr_valuesandmax_ageinWWW-AuthenticateandacrValues/maxAgein the body, including the requirements of break-glass grants and role activations, whose denials now carry theassurancegrantee into.permdock doctorPD036 warns on aprotect(permission)with no row loader on a route whose path names an id. -
Link capabilities. A share link is a signed
permdock-capability+jwtwhosecapabilityclaim (v1) holds roles on one resource instance (on), optionally narrowed topermissions, with a requiredexpiresAt, an optionalredeemer('anyone','signed-in',{ user },{ scope, id }) and optional one-time use.signCapability(input, signer, { audience })issues one from permission and resource references, andcapabilitySubjectandparseCapabilitybuild the subject and validate the claim.subjectFromCapability(token, { issuer, audience, revoked?, replay?, viewer? })inpermdock/jwtverifies it and returns akind: 'link'principal whose only membership is{ on, roles, via: 'link' }, withdelegation.scopesfrompermissions; a revoked link id, a replayed one-timejti, a redeemer the viewer does not satisfy or a throwing store resolves to the anonymous subject, reported onon('auth')with the new causescapability-revoked,capability-replayedandredeemer-mismatch. A tenant can tighten links on its resources with aLinkPolicy(maxLifetime, allowedredeemers, requiredonce):subjectFromCapability({ linkPolicy })refuses a violating link with causelink-policy,signCapability({ linkPolicy })refuses to sign one, andlinkPolicyViolationnames the broken rule.subjectFromJwtnever accepts the capabilitytyp.For RLS,
exchangeCapability(subject, { key, alg, kid?, ttl? })inpermdock/supabaseexchanges a verified link for a short-lived Supabase access token withrole: 'anon'and the capability in acapabilityclaim, andpermdock rls generate --capabilities(rls.capabilities) emitspermdock_capability_ids(p_resource, p_role, p_permission)and oneanonpolicy per resource-scoped grant; a resource role without a memberships table is then reached by links only instead of failing generation.permdock/testingships thepermdock-capability+jwtfixture. -
Named scopes (breaking).
definePolicy({ scopes })now declares scopes by name and in order, each{ key, within? }:{ organization: { key: 'organization_id' }, customer: { key: 'customer_id', within: 'organization' } }. Every scope after the first names an earlier parent, so the scopes form one tree;tenantandteamare aliases of the first and second scope, and a policy with{ tenant, team }must now declareteam: { key, within: 'tenant' }.role(name, grants, { on })is typed against the declared names, and theactivationandrestrictedrole options are reserved.Memberships are
{ scope, id, within?, roles, via?, expiresAt? }(or{ on }); the{ tenant, team }form is still accepted as input and normalised when the subject is resolved, and snapshots, claims and events always carry the canonical form. There is no implicit cascade between scopes: a role applies only through a membership of its own scope, so a team membership no longer satisfies a tenant role, andsubjectFromJwtgroups anddirectoryMembershipSourcenow yield tenant memberships withvia: 'group:<id>'instead of team memberships.definePolicythrows when a scoped instance grant touches a resource without amemberOfrelation on the scope's key.where(),filter, snapshots (scopesis now an ordered{ name, key, within, resources }list),fromSnapshot,mayAccess,memberOfconditions, custom roles (CustomRole.scopeandid) and the catalog (scopes[],roles[].onas a scope name) understand named scopes.permdock rls generateemits onepermitted_<scope>_ids(p_grant)per declared scope, readsrls.memberships.scopes.<scope>tables withcolumnsindatabasemode and canonicalmembershipsclaim entries injwtmode, types scope columns throughrls.scopeTypes, and keys the custom-role tables byscopeandscope_idinstead ofteam_id.authorizeSql({ scope })names the first scope.permdock doctorPD025 (--only scopes) warns on fixture memberships the scopes would drop.rlsParity({ snapshot: true })also checks the serialized snapshot, andtestMembershipSource({ policy })checks memberships against the policy's scopes. -
New runtime entry
permdock/catalog:parseCatalog(json)validates apermissions.catalog.json(text or parsed) againstschemas/catalog-v1.jsonand returns a deep-frozen, prototype-safe copy, throwingPermDockValidationErrorwith every issue;rowConditionKeys(catalog)lists the permissions markedrowConditions: true; theCatalog*types move here and stay exported frompermdock/cli, withCatalogDocument['version']now1.permdock/cliexportscatalogPath(config, cwd, out?), the pathcollectwrites to.catalog-v1.jsonnow requires whatcollectalways writes (generatedAt,generator, a permission'smetaandusages, a resource'sidandschema), types every field the catalog carries, and adds the missingstaleOnon approvals;permdock catalog --format schemaemits the same document. -
One name per concept, and the policy vocabulary reaches every handler.
Adapters that hand out an instance now type it with the policy's roles, plans and permissions (
PermDock<V>), as core andpermdock/nextalready did: Hono'sc.get('permdock'), Express and FastifywithPermDockhandlers, Elysia's derivedpermdock, the Node, Claude Agent SDK, Eve, OpenAI Agents and terminalpermdock()results, the terminalprotectcontext, the Supabase middleware contribution, oRPC middleware context, Convexctx.permdockandPdpPermDock.permdock/fastifygainswithPermDock(handler), which typesrequest.permdockwithout augmentingFastifyRequest.Old New CreatePermDockOptionsPermDockOptionsCreatePermDockPluginOptionsPermDockPluginOptionsServerPermDock.handler,AuthzenPermDock.handlerpermdockHandlerExpressPermDock.handler(fn)withPermDock(fn)SsfAdapter,SsfOptionsSsfPermDock,SsfPermDockOptionsSupabaseMiddlewareOptionsSupabaseMiddlewarePermDockOptionsA2AAgentCard,A2APermDockand the otherA2A*typesA2aAgentCard,A2aPermDock, …dock,pd,server,factoryfor the instance in docs and skillspermdock; acloud()result ispermdockCloud -
One package: the CLI moves into
permdock. Thepermdockbinary is the package'sbin,@permdock/clibecomespermdock/cli(defineConfig,run),@permdock/cli/unpluginbecomespermdock/unplugin, andpermdock/next/pluginrunscollectitself instead of resolving@permdock/cli.permdock, owned by thescaledockhqnpm organisation, is the only package name.Runtime entry points (
permdock,permdock/next,permdock/jwt, ...) still depend on@standard-schema/speconly: every command is loaded on demand, andtests/bundlefails if a runtime entry reaches a CLI or test-runner package. Dependency cost, measured as npm unpacked size:oxc-parseris the package's one other dependency, becausecollect,usage,doctor,catalog,cloud pushand the build hooks parse source: 1.43 MB, plus@oxc-project/types(0.04 MB), plus one platform binding (1.59 to 2.12 MB; darwin-arm64 1.76 MB, linux-x64-gnu 2.12 MB). That is about 3.1 to 3.6 MB per install.ajv(1.03 MB unpacked) andyaml(0.69 MB) are bundled into the lazily loaded OpenAPI command chunks instead of being installed.pgsql-parser(2.83 MB withlibpg-queryWASM andpgsql-deparser),pg(0.10 MB) andunplugin(0.08 MB plus its dependencies) are optional peers.permdock rls import, the--dbmodes andpermdock/unpluginprint the install line when the peer is missing.
Generated catalogs record
generator: permdock@<version>, and generated barrels say@generated by permdock. -
OpenAPI output now matches the documented shapes:
scheme.deprecatedemitsdeprecatedon 3.2 and 3.3 andx-oai-deprecatedon 3.1.- On 3.1 the device flow sits inside
flowsasx-oai-deviceAuthorization, with the flow's own URL asx-oai-deviceAuthorizationUrl. - A permission granted unconditionally to
anyone()getssecurity: []. - Top-level grants now reach
x-permdock-conditionsandx-permdock-approval. - The 3.3 profile scheme carries
supportedParametersSchemaas a URL and namedservers.securityProfileRequirements(scopeSets?)writes one requirement per distinct scope set, each withsecurityScheme,token_endpoint_auth_methods,grant_typesandscopes. permdock openapi emit --target 3.3writesopenapi: 3.3.0and validates the result against a patch of the 3.2 schema keyed by the draft pin.permdock openapi importaccepts 3.3 documents.
-
Overlay actions now target operations with
$.paths.*[?@.operationId == '<id>']. The old target had an extra.*and selected nothing.permdock openapi emit --format overlaycovers the source document's operations by their ownoperationId, fails on a covered operation without one, and under--checkreports operations whose source already setssecurity.overlay({ operations })takes the same list. Actions are sorted, andinfo.versionis a catalog fingerprint.x-permdock-conditionsandx-permdock-approvalare objects keyed by permission key, as the extension table defines. -
Permission-level custom roles bounded by a ceiling.
CustomRolegainsgrants({ permission, effect? }, no conditions) and an optionalteam;includesis now optional. Every custom role resolves throughresolveCustomRole(policy, role): its included roles' grants plus its own allows, minus its own denies, intersected with the code allows of the declaredassignableroles in its scope. Grants inherit the declared grant's condition, approval and limit, and keys outside the ceiling are dropped and reported byvalidateCustomRoleandpermdock doctorPD023.assignablePermissions({ tenant? })joinsassignableRoles: both intersect the ceiling (narrowed byRoleSource.assignable) with what the subject holds, and a held role or granted permission withmeta.manageRoles: truelifts the intersection. Snapshots carry resolved custom-role grants and a per-tenantassignablelist, read byfromSnapshotand the newuseAssignablePermissionshook (Vue, Solid, React Native;assignablePermissionsstore in Svelte).testRoleSourceaccepts{ policy }to check custom-role grants against the ceiling.Behaviour change: an
includesentry is now bounded by the ceiling too, so a custom role that included a global or non-assignable role no longer reaches that role's grants, andassignableRoles()now also lists assignable roles whose every allow the subject holds. -
Provisioned roles and plans are bound to their tenant.
directoryMembershipSourcelooks users up with a structuredeqfilter, so a principal id is never parsed as filter syntax. Core drops roles the policy declaresassignable: falsefrommanagedBy: 'idp'memberships, andscimHandlerreports unknown roles onPATCHtoo.subjectFromClerkputso:plans (asentitlements) ando:features on the session organization's membership instead of the principal. -
RLS CLI fixes found by the coverage suite:
rls importmaps= 0and= falseconditions instead of importing them as opaque.rls importtreats onlyauth.jwt()andauth.session()as claim sources;->>on a column or on(select auth.uid())is no longer aprincipal.claim.*reference.- The raw
rls importfallback reads roles,forandas restrictivefrom the policy header only, so awith check (...)or a word insideusingno longer leaks into them. - A malformed
supabase.hook.membershipsentry gets the "takes fromTable / fromJunction sources" error instead of aTypeError. rls generaterefuses any output that namesservice_role, in every target; before, the guard matched onlyto,fromand;forms.
-
Relationship graph additions. An edge relation takes
match(fixed column values, so one table with arolecolumn serves several relations) andgroups(a row may name a group whose members hold the relation, nested at most 16 deep per resource), and any relation may listincludes, the relations that imply it. A resource declareslinks, named to-one references, andrelation(resource, name, { through: ['folder', 'team'] })follows them. A resource role on a self-parented resource reaches the instances below it when arelationssource is present.RelationHoldergains agroupvariant,RelationSource.ancestorstakes a link name asthrough,whoCanreports group shares and implied relations, and RLS compiles all four through thepermitted_<resource>_idshelpers and newpermdock_link_<resource>_<link>helpers. -
Relationship graph. A resource's
parentmay now name the resource itself (nested folders, sub-teams, reporting lines),restricted: '<column>'names a boolean column whose rows are reached only by grants on themselves, and relations take three shapes: a field (owner: 'ownerId'), an edge table ({ edge, object?, subject?, expiresAt? }, where an expired edge does not match) and, on a principal resource,{ principal, period?: { startsAt?, expiresAt? } }.relation(resource, name, { through: 'parent', depth })follows the row's parent chain upward to the relation's resource and then along its self-parent for up todepthhops (default 16, at most 32);definePolicyrejects athroughgrant that cannot reach its resource or walks amemberOfrelation.Graph grants compile to the new
relatedcondition and are decided through aRelationSource(ancestors,related) passed ascreatePermDock({ relations })(every adapter'screatePermDockforwards it), with answers cached per instance.memoryRelations(permissions, { rows, edges })is the in-process source andtestRelationSourceinpermdock/testingthe conformance runner. Evaluation stays synchronous:await permdock.loadRelations(permission, rows)loads an async source's answers first. A cycle, or a chain that goes on pastdepthwith no holder within it, denies with the new reasonrelation-depth; a missing, throwing or unloaded source denies withrelation-unavailable, and a graph deny that cannot be evaluated denies the decision.await permdock.whoCan(permission, row)lists holders and how (role,relation,share) fromMembershipSource.listandRelationSource.related, never grants, and returnscomplete: falsewhenever a grantee cannot be enumerated.Snapshots carry no graph: graph grants, and grants on a relation with a
period, areportable: falsethere, so clients resolve them on the server;where()keeps graph grants asrelatednodes and leaves out only relations with aperiod(partial: true).permdock rls generateemits<schema>.permdock_closure(resource, ancestor, descendant, depth)kept current by statement triggers on each walked self-parented table (stopping at restricted rows and the depth, refusing cycles),permitted_<resource>_ids(relation)helpers, and compiles each graph grant to one uncorrelated closure subquery.permdock rls verify --tree --dbchecks parity on a generated tree with restricted branches. Catalog resources carryrestrictedand the new relation shapes, and a relation grantee on the wire may carrythroughanddepth.permdock doctorPD031 warns on a self-parent orrestrictedcolumn no graph grant uses, and PD032 errors on a graph resourcerls generatecannot name. The saas domain inpermdock/testinggains a folder tree with shares (saasRelations,saasFolderScenarios). -
Repository tooling only: presets for lint, TypeScript, tests, CI, Turborepo and Vercel, plus Portless local dev, editor and MCP configs, agent docs and decision records. The published
permdockpackage is unchanged. -
Restricted API keys and service accounts. An API key is an opaque
pdk_<id>_<secret>; the application stores its SHA-256 hash (hashApiKey) next to aCredential(v1) record and never the key (generateApiKey,parseApiKey). Ausercredential acts as its owner with the owner's live roles and memberships, narrowed to the key's permissions throughdelegation(OAuth scopes, and RFC 9396authorizationDetailswith anidentifierper resource id), so a demoted owner's keys lose the same rights at once. Aservicecredential is akind: 'service'principal whose only membership is{ tenant, roles, via: 'credential' }.decideCredential(permdock, request, { settings, approved? })decides whether the current subject may create a key: a service key's roles and permissions must be within the creator'sassignableRolesandassignablePermissionsin its tenant, and links, credentials and delegated creators cannot exceed themselves (new denial reasonexceeds-creator). A tenant'scredentialssettings (maxTtl,kinds,approval,allowNoExpiry) come from the newSettingsSource(memorySettings); a violation is the new denial reasoncredential-policy, a key without expiry is refused unless the tenant allows it, andapprovalmakes creationapproval-requiredwith a token bound to the key and its creator.credentialSubject,credentialDelegation,credentialPolicyViolationandparseCredentialare exported.subjectFromApiKey(options)inpermdock/serverreturns aSubjectResolverthat verifies a key through aCredentialVerifier(apiKeyVerifier({ find })compares hashes in constant time;memoryCredentials()issues, rotates and revokes), re-checks expiry,revoked(id)and the tenant settings on every use, and loads the liveowner; any failure is the anonymous subject with anon('auth')cause (malformed,unknown-credential,invalid-claims,expired,credential-policy,credential-revoked,owner-unavailable). Credential changes and sampled uses arecredentialsink events (created,usedwithsample,rotated,revoked) with CloudEvents typedev.permdock.credential, built bycredentialEvent, and decision events name the key insubject.credential.permdock/testingaddstestCredentialVerifierandtestSettingsSource, andpermdock doctorPD029 (--only credentials) flags fixture keys that never expire and tenants that allow them. -
Role ownership and audiences.
role(name, grants, options)takesminandmax(holders per scope instance),transferOnly(the holder count only moves by transfer),assigns(the roles a holder may assign and revoke),for(the membership kinds,Membership.via, that may hold the role) and a typedmeta: RoleMetawithaudience. A role held through a membership kind itsfordoes not list, or through a membership withoutvia, grants nothing: it is dropped when the subject is resolved and filtered in the generated RLS helpers.permdock.decideRoleChange({ kind: 'assign' | 'revoke' | 'transfer', role, scope, id, within?, target, holders? })checks one change against theassignsgraph, the ceiling,for,exclusiveWith,min,maxandtransferOnly, with the new denial reasonslast-holder,max-holders,transfer-only,not-assignable-by,self-demotion,not-allowed-for-membershipandconflicting-role; the snapshot-backed client denies it withunsupported. Once any role declaresassigns,assignableRoles()returns exactly the roles the held roles list, andheldRoles()/assignableRoles()are ranked by the graph.heldRoles({ scope, id })narrows to one scope instance, andpermdock.audiences()andsnapshot.audienceslist the distinct audiences of the roles held in the active tenant.permdock rls generatereads aviacolumn on membership tables, adds a deferred constraint trigger per scope table forminandmax, statement triggers over transition tables fortransferOnly, andpermdock_can_assign(p_role, p_scope_id). Catalog roles carrymin,max,transferOnly,assigns,for,exclusiveWithandaudience.permdock doctorPD026 (--only ownership) warns on a scope whose roles set nomin. -
SCIM discovery follows RFC 7644 section 4:
/Schemasand/ResourceTypesreturn ListResponses, serve one item by id, carrymeta, and refuse afilterwith 403. Every advertised attribute statesmultiValuedandrequired. Created users and groups now get a realmeta.createdtime instead of an empty string. -
Smaller server bundles, bounded network calls and a faster policy lookup.
OpenTelemetry and Web Bot Auth now reach an app's bundle only when it imports them. The adapter options take the function instead of its options, and
applyOtelis gone:Before After otel: { logger }otel: (permdock) => withOtel(permdock, { logger }), withwithOtelfrompermdock/otelwebBotAuth: { verify: true, keys }webBotAuth: (request) => verifyWebBotAuth(request, { verify: true, keys }), withverifyWebBotAuthfrom the adapter entry orpermdock/serverOtelWrap(frompermdock/otel) andWebBotAuthVerifier(frompermdock/server) type the two options. A Hono app without either option ships about 1.6 KB gzip less;permdock/nextandpermdock/mcpabout 1.9 and 2.4 KB less.Every outbound request now has a default timeout: JWKS and discovery 5 seconds (
jwksCache.timeoutchanges it), Web Bot Auth key directories 5 seconds, Cloud requests 10 seconds, the React provider's endpoint calls 10 seconds, terminal device, token and CI OIDC requests 10 seconds, and SSF polls 30 seconds. An aborted request fails closed like an unreachable endpoint. Concurrent JWKS and discovery fetches for one issuer share a single request.definePolicyindexes grants by permission key once, so a check no longer scans every grant.The CLI reports a missing optional peer (
pg,pgsql-parser) with its install line only when the module is missing; any other load error surfaces as it is. -
Snapshots bind
principal.claims.*refs in grant conditions to the subject's values. The snapshot principal carries no claims, so a client read them as missing: aneqorinon a claim denied what the server granted, and anotIngranted what the server denied.principal.id,tenantand the other fields the snapshot carries stay references. -
Soft and hard quotas.
limitacceptsmode: 'hard' | 'soft'(default'hard', today's behaviour) andalertAt, a fraction ofcountin(0, 1]. A soft limit grants past its count with the obligation{ kind: 'over-limit' };alertAtadds{ kind: 'near-limit' }once usage reaches it. A granted decision under a limit carriesquota: { remaining, resetsAt }(resetsAtin Unix seconds), andobligationswhen one applies. An unreachable store still denies withlimit-unavailablein soft mode.describePolicymatrix cells acceptobligations(the expected kinds). New exported types:GrantLimit,Quota,Obligation. -
Subject attributes in RLS.
permdock rls generatecompiles nested claim refs (principal.claims.attrs.regionbecomes(select auth.jwt()) -> 'attrs' ->> 'region', with every segment checked against the prototype-key blocklist), casts a claim to the column's type read from the resource's Standard JSON Schema (numeric,boolean,timestamptz,date,uuid; numbers and booleans must be that JSON kind), and compilesin/notInagainst an array claim to= any (array(select ...)), evaluated once per statement. A grant that readscontext.*is refused with a pointer to the new doctor check PD027 (--only context-refs), or skipped under--skip-closures.rlsParityaccepts subjectclaimsand aneondialect. -
Supabase SQL security and performance:
- Helpers.
rls generateandsupabase hook generateput thesecurity definerhelpers,role_permissions, the closure, break-glass andauthz_versiontables and the hook in a privatepermdockschema the Data API does not expose.authz_version.user_idis auuidthat referencesauth.usersand cascades on delete. - Casts. The helpers, the hook, the ownership triggers, graph SQL and
rls verify --treecompare a membership's user and scope columns uncast and cast the parameter instead, so the column's index applies. authorize(). It honourseffect = 'deny'and membership expiry, and is executable byauthenticatedonly.- Break-glass. The break-glass read keeps the tenant boundary: a grant on a role reads only the rows of the scope instances where the subject holds it, through a
<permission>#break-glassgrant key. - Generated SQL. It enables row level security before the grants and schema-qualifies every table. The hook reads
app_metadatafrom the event instead ofauth.users. - New
rls generate --splitparts.seedswrites therole_permissionsrows as a versioned migration (--seeds-out).indexeswrites the indexes the policies and helpers filter through.rls verify --introspectwarns about a column no index starts with. - New doctor checks. PD046 to PD053 cover
user_metadatain SQL, definer functions executable byanonorpublic, functions withoutsearch_path,publictables without RLS, unwrappedauth.uid(),updatepolicies withoutwith check, and unindexed foreign keys. PD054 flags stalerole_permissionsseeds. PD028 accounts for Supabase's default privileges and tracksinsertandupdateapart. rls.anonymousSignIns: 'deny'keeps a Supabase anonymous sign-in out of every grant butanyone()'s.exchangeCapabilitysignsES256by default.- Contracts. Every catalog permission carries
rowConditions, which istruewhen the catalog was built without the policy. The claims schema andsupabaseClaims()require a non-emptysubat eachactlevel and acceptmember: { group }, whichsubjectFromSupabasekeeps.
- Helpers.
-
Supabase declarative schemas on pg-delta. With
[experimental.pgdelta] enabled = trueinsupabase/config.toml,permdock rls generate --splitwithout--outwrites pg-delta's per-schema, unnumbered layout underdeclarative_schema_path:permdock/helpers.sql,permdock/indexes.sql,public/policies/permdock.sqlandpermdock/functions/custom_access_token_hook.sql.permdock supabase hook generatewithout--outwrites the same hook path. pg-delta keeps grants, so the hook keeps itssupabase_auth_admingrants. It rejects data in a schema file, so thehelperspart now needs theseedspart with--seeds-out. Doctor PD042 and PD043 are off under pg-delta. The token hook castsuser_idtouuidonce, sosupabase db lintreports no implicit casts. -
Suspension in generated RLS, and a token hook that writes memberships.
rls.suspension(also asupabaseRlsandauthorizeSqloption) names a users table and a table per scope, each{ table, id, disabledAt?, status?, active? }.permdock_has, everypermitted_<scope>_idsandauthorize()then drop a suspended user's roles and every membership whose instance or ancestor instance is suspended. The check runs live indatabaseandjwtmode, and a missing status row counts as suspended.generatefails on a suspension entry with an undeclared scope, nodisabledAtorstatus, astatuswithoutactive, or a nested membership table without the ancestor column it needs.In
jwtmode,permdock rls generate --rbac supabasenow emits acustom_access_token_hookthat also writes the canonicalmembershipsclaim ({ scope, id, within?, roles, via?, expiresAt? }) from everyrls.memberships.scopestable. It leaves out expired and suspended entries, gives a suspended useruser_role: []andmemberships: [], and grantssupabase_auth_adminread access to the tables it reads.supabaseRls({ memberships })acceptsscopes, andSupabaseMembershipTableacceptscolumns, likeRlsMembershipTable. -
The OCSF projections now match OCSF 1.3.0.
accessToOcsfuses Account Change activity 2 (Enable) forstartedand 5 (Disable) forendedandrevoked, instead of 1 (Create) and 4 (Password Reset), and sets the requiredtype_uid.toOcsfalways sets the requireduser, as{ name: 'anonymous' }for an anonymous subject. -
The SQL helpers have a written contract:
permdock_has,permitted_<scope>_idsandpermitted_<resource>_ids, what each answers, the grant keys to pass, the role-and-scope-only rule and the exact-text id rule, on the RLS page.schemas/supabase-claims-v1.jsonin the package is the JSON Schema of the claims the hook writes and the helpers andsubjectFromSupabaseread. The Supabase page adds a "With better-supabase" split of what each package owns. -
The SSF receiver looks up
onEventhandlers by own key only. An event named after anObject.prototypemember, such astoString, now reaches the*handler instead of calling the inherited function. -
The Supabase hook manifest gains
memberships(each source's table and its user, scope, id, role,within,viaand expiry columns or fixed values),rls(helper schema,jwtordatabasemode, tenant claim, scope id types, and each helper's arguments, return type andexecuteroles),decidingColumns(everyschema.table.columna membership or anattrsclaim is computed from, the same set PD028 checks) andmarkers({ hook: 'v1', grants: 'v1' }), plus$schema. Its JSON Schema ships asschemas/supabase-manifest-v1.json.permdock supabase inspect --out permdock.manifest.jsonwrites it, and--checkexits 1 when the file's JSON differs.inspect --outused to override the hook path; the path now always comes fromsupabase.hook.out.fromTableandfromJunctionexpose their entry assql.manifest. -
The Supabase manifest's
rlssection has amembershipslist of the tablesmember_<scope>_ids_forreads, in the same shape as the hook'smemberships. It names therls.membershipstable mapped for each scope, else therls.membershipSourcesthat can hold it, else the hook's sources. A reader that resolves memberships outside the hook, such as better-supabase's entitlements, can now read the tables the SQL helpers use. -
The Supabase token hook migration adds
permdock_bump_authz_version_for(p_users uuid[]), which bumpsauthz_veronce for each listed user. A trigger on a table the hook does not read, such as better-supabase'sentitlement_members, can now invalidate its users' tokens. The function issecurity definer, and no client role may execute it. The membership triggers now call it. The manifest names it inauthzVersionBumpwhenauthzVersionis true. -
The
cloud()sink bounds its re-queue while the Cloud is unreachable (capacity, default 10 000, oldest dropped first), matchingmemorySink. -
The
permdockCLI parses flags with citty.permdock <command> --help(orpermdock help <command>) prints that command's flags with their values and defaults. A flag with a fixed set of values rejects any other with exit2and the list it accepts, for examplecatalog: Invalid value for argument: --format (xml). Expected one of: json, schema, markdown.Config, permissions and policy modules that Node cannot load on its own fall back to jiti, so they may use
enum, TSX, extensionless relative imports and thepathsaliases of the nearesttsconfig.json.--checkoncollect,openapi,rls generateandsupabase hook generateprints a unified diff of each stale file, cut to 40 lines.A missing optional peer (
pg,pgsql-parser,unplugin) fails with the install line for the package manager that ran the command, or the one whose lockfile the project has.On a terminal,
skills installwithout--agentasks which agents to install for, anddoctor --fixasks before writing. In CI, in a pipe or with--jsonthere is no prompt.doctorreports are styled on a colour terminal;NO_COLORand--no-colorturn styling off.supabase/config.tomlis read with a TOML parser, and the newPD045warning reports a file that does not parse.The CLI's new dependencies are
citty,jiti,smol-toml,package-manager-detector,@clack/promptsanddiff. Runtime entries still depend on@standard-schema/speconly. -
The
permdockCLI starts faster and reports failures in a form scripts can read.permdock --version(-v) prints the version.--helpprints a static command list and loads no command, no config and no core.--yes(-y) never prompts and takes the answer the flags give.- Under
--json, a failure prints RFC 9457 Problem Details on stdout, withtypehttps://permdock.dev/problems/cli-usageorcli-unavailable. - A database that does not answer exits with
1, not2, and names the command.--dbconnections time out after 10 seconds and statements after 60.rls introspectruns its queries in parallel over a pool of four. Ctrl-C ends the connection and exits with130. - Generated SQL, catalogs and
who-canoutput sort by code unit, so the same input gives the same bytes in every locale and runtime.
-
The
permdockskill names the docs MCP toolssearch,list_pagesandget_page, the tools the docs server at/mcpnow exposes. -
The
subjectoption ofpermdock/serverand every adapter built on it is now typed to return a fullSubjectornullas well as the policy's user, matching what the kernel already accepted at runtime. -
The bundled Agent Skills are renamed
permdock-wireandpermdock-audit, and six skills join them:permdock, a general skill that routes to the others, pluspermdock-agents,permdock-approvals,permdock-tenancy,permdock-dataandpermdock-credentials.permdock skills installcopies all eight, andpermdock doctor(PD005) looks for thepermdockskill. -
The catalog,
permdock rlsandpermdock doctornow call Standard JSON Schema'sjsonSchema.outputwith{ target: 'draft-2020-12' }, as the spec requires, instead of with no options. -
The generated Supabase hook starts with a stable
-- permdock:hook v1 schema=… tenant=… budget=… claims=…line, andsupabase hook generate --checknames the marker fields that drifted.permdock supabase inspect [--json]prints theversion: 1manifest of the hook and SQL helpers (helper names, tenant claim, budget and its measure, claims written,authz_ver);permdock/supabaseexports theSupabaseHookManifesttype andpermdock/testingexportssupabaseHookManifestFixture. -
The npm README now covers install requirements (Node.js 24 or later, TypeScript 7), the quick start, one example per surface and every entry. The bundled
wire-permdockandaudit-permissionsskills declarelicense: MITandmetadata(author,homepage,repository) in their frontmatter, and the repository exposes them to Claude Code as thepermdockplugin in.claude-plugin/marketplace.json. -
The package carries the
tanstack-intentkeyword. Internal non-null assertions andanyflows in the core, conditions, CLI and adapters are replaced with checked narrowing; the PDP adapters' granted decision now fails closed withanonymouswhen a subject has no principal instead of asserting one. -
The test runners move into
permdock:@permdock/testingis now thepermdock/testingsubpath, withpermdock/testing/saasandpermdock/testing/saas/permissions. Vitest is an optional peer, and no application entry imports the testing entries. Import frompermdock/testingand drop the@permdock/testingdev dependency;permdockis the only package name. -
Tokens are bound to their issuer and audience.
subjectFromJwtrequiresissuerwith any remotejwks(a URL as well as a key set).joseTokenVerifierandpermdock/ssftake the issuer fromdiscoverywhen none is set and check every token against it.subjectFromIntrospectionrequiresaudto contain the configuredaudience, rejects a differingiss, and no longer fillsprincipal.issuerfrom the options.scimHandlerno longer derives the audience from the request URL and throws whenverifieris set withoutaudience. -
PermDockDeniedErrorandPermDockApprovalRequiredErrorcarry a stabledigest(PERMDOCK_DENIED;<permission>andPERMDOCK_APPROVAL_REQUIRED;<permission>;<token>), the property React keeps when an error crosses from a Server Component to the client, andparsePermDockDigestreads it. The newpermdock/next/cliententry exportsPermissionBoundary, an error boundary built oncatchErrorfromnext/errorthat renders itsdeniedorapprovalfallback for a thrown PermDock error and lets every other error through, andusePermissionBoundary(), which gives a fallback the permission, the approval token andretry(). -
RateLimitandRateLimit-Policynow serialise the policy name as a valid RFC 9651 sf-string:"and\are escaped instead of dropped, and a role name outside printable ASCII is percent-encoded as UTF-8. -
cloud().approvalsimplementscancel(filter, meta)overPOST /v1/environments/:env/approvals/cancel, so session revocation rejects pending Cloud approvals in one call. -
cloud().policies.refresh()verifies thepermdock-policy+jwtagainst the Cloud environment URL (issandaudare<PERMDOCK_CLOUD_URL>/v1/environments/<env>;expis 24 hours after issue). Theaudienceoption is removed.cloud()exposes the environment URL asissuer,jwksis the environment's JWK Set URL, andcloudEndpoints()resolves both without a key. The policy JWS fixture inpermdock/testingfollows the new claims. -
cloud().snapshots.get()requestsapplication/jwtand returns only a compactpermdock-snapshot+jwt; an unsigned snapshot body now throws instead of being parsed. -
createPermDockfreezes a copy of the subject instead of the caller's user object, roles and context, so later writes to them neither throw nor reach the instance. A schema that returns a rejecting Promise at a boundary, in a claims mapping or in WebMCP tool arguments no longer leaves an unhandled rejection behind. -
decide()evaluates a denial'salternativesat thenowyou pass, not the wall clock. With a pinnednowand no sink oron()listener, a check reads no clock at all, so it runs in a prerendered React Server Component or Client Component undercacheComponents. -
definePermissions(tree, { renamed })maps former permission keys to current ones: stored custom roles, OAuth scopes, AuthZEN actions, hosted grants andfindPermissionaccept the old key, while decisions and audit see only the new one. The catalog listsrenamedFrom,permdock diffreportsrenamed(not breaking) andalias-removed(breaking),rls generateseedsrole_permissionsunder former keys too andpermdock_custom_keysreads a stored former key as its current key,rls generate --shimsadds wrappers under legacy helper names, andpermdock doctorPD055 and PD056 report what still uses an old key or helper. -
definePolicytakesdelegations: standing statements that holders offrom(a role,authenticated(), a plan orassurance()) let actors matchingto(actor('eve'), an actor kind, or{ kind, id }for one agent) usepermissionsfor them, with optionalvalidFrom/validUntil. Each is normalised to{ from, to, permissions: string[], validity? }onpolicy.delegations, is part of the fingerprint, and is listed in the catalog's newdelegationssection;permdock diffreports a removed one asdelegation-removedand one that lost permission keys or validity asdelegation-narrowed. Indecide, the matching delegations form a ceiling for the actor: a permission outside it isnot-delegated, inside it a tokendelegationon the call must still cover, and the principal's grants, conditions, denies and approvals apply first as before. The snapshot carries the ceiling asdelegatedand the client evaluator applies it.delegatedPermissionsis exported frompermdock. -
dev.permdock.catalogdrift findings are typedCatalogFindingobjects{ code, permission, grant? }withcodeone ofpermission-removed,not-hostable,grantee-removedorapproval-tightened;parseCloudEventandverifyWebhookreject the earlier free-text strings. -
endpoint: falseoncreatePermDockfrompermdock/next, itsPermDockProviderand thepermdock/reactprovider makes the client snapshot-only. It never fetches, and a check the snapshot cannot answer (a closure, graph relation or period grant) is denied with the new reasonserver-only. The provider logs the first such permission once. The same reason replacesopaque-conditionwhenever the client store has no endpoint.permdock doctorPD044 warns whenusePermissionreads such a grant and the app has neither anapp/**/api/permdock/route.tsusingpermdockHandlernor anendpoint. The Next.js Cache Components guide adds URL slugs (a public slug lookup ahead of the private snapshot,notFound()andforbidden()), snapshot-only mode, and a cross-origin endpoint section. -
isPermission(value)is exported frompermdock: it narrows anunknownsuch as an MCP tool'smetatoPermissionwithout a cast, and accepts a leaf that crossed JSON.permdock/cliexportsparseHookMarker(sql)andparseGrantsMarker(sql), which read the first line of a generated hook or--grants-outmigration. -
mayUse(permdock, permission)is exported frompermdock. It answers whether some grant in the instance's snapshot could match for its subject, active tenant and delegation, and is the listing hintpermdock/mcpandpermdock/a2aalready use. An MCP server thatprotectServercannot wrap, such as better-supabasecreateMcp, filters its tool list with it and decides each call withdecide;adapters/mcp.mdxhas the recipe. -
membershipsink events name the scope instance the way memberships do (breaking).MembershipEventdropstenantandteamforscope,idandwithin, and gainsexpiresAt, so a role change at any depth (a contact on a site inside a customer inside an organization) is recorded.membershipEvent()takes the same fields and throws aTypeErrorwhenscopeandidare not given together, orwithinhas noscope. SCIM group changes emit{ scope: 'tenant', id }, and Better AuthonRoleChange({ sink })emits{ scope: 'team', id, within: { tenant } }for a team role. The format staysv1. -
permdock collectandpermdock doctorno longer crash withCannot read properties of nullon a source file with array holes (const [, , id] = parts). Doctor PD010 and PD014 ignore comments, and PD014 no longer reads aconst jwks: JSONWebKeySetdeclaration as ajwksoption without an issuer. -
permdock diff a bcompares two policies or catalogs (apermissions.catalog.jsonor a module exportingpolicy) and lists the permissions, scopes, roles and grants that changed. It exits1on a breaking change: a removed permission, scope, role or allow, a narrowed allow (new or changedwhere/check, new approval, fewer fields, shorter validity, new limit), or a new or changed deny.--impactruns therls verifyfixtures through both policies and reports who loses or gains what; a lost grant is breaking.--jsonprints the report. The catalog gains agrantssection listing every code grant in canonical order when built with the policy;CatalogGrantandCatalogValidityare exported frompermdock/catalog. -
permdock doctorPD002 andpermdock collectno longer report valid code as unknown permission references. The false positives were role and plan vocabulary (role(roles.admin, …),plan(plans.pro)), a leaf's wire fields (permissions.post.read.scope,.key) and a resource subtree (relation(permissions.post),include: [permissions.post]). -
permdock doctoradds two Supabase checks. PD040 warns onauth.role()in a migration, which Supabase deprecated; name the policy's roles withto authenticatedinstead. PD041 warns whenexchangeCapabilitysigns withalg: 'HS256', the project's shared JWT secret; sign withES256and an asymmetric signing key. -
permdock doctorwithout--fixandpermdock usageno longer writepermissions.catalog.json. PD002, PD044 andusagescanned the sources through a fullcollect, which wrote or refreshed the catalog as a side effect, so a doctor run in CI left a changed file behind. They now scan in memory, as theusagepage already said. -
permdock rls generate --rbac supabaseno longer emitscustom_access_token_hook:permdock supabase hook generateis the one generator of the token hook, so a single function writesuser_role,membershipsand the other claims. The scaffold keeps the enums,user_roles,role_permissions, the helpers andauthorize(). -
permdock rls generate --split helpers,policies,hook --out <dir>/056_permdock_{part}.sqlwrites one file per part for Supabase declarative schemas, and--grants-out <file>(or-to print) moves the token hook'ssupabase_auth_admingrants, execute revoke and read policies into a migration of their own, sincesupabase db diffdrops them.permdock supabase hook generatetakes the same--grants-out.--checkcompares every part and the grants file and names the part that drifted. Doctor adds PD042 (error: the hook is declared undersupabase/schemasand no migration from the one that creates it on grants it tosupabase_auth_admin) and PD043 (warning:schema_pathsapplies a file that calls the helpers before the helpers part), anddoctor.migrationsnow defaults tosupabase/schemastoo. -
permdock rls generate --target drizzlewritespgPolicy(...).link(schema.<table>)exports with thedrizzle-orm/supabaseroles, and--target prismawrites Prisma 8policy_*blocks and refuses deny grants, which Prisma 8 cannot express. Both targets also write<out>.migration.sqlwith the helpers, grants andenable row level security, and--checkcompares both files.rls.drizzle.schema,rls.drizzle.exportsandrls.prisma.modelsset import paths and names. -
permdock rls generateapplies a role'sforto resource-role and nested-scope membership tables too: theexistscheck counts a membership only when itsviacolumn lists an allowed kind, and a table without aviacolumn holds such a role for nothing, matchingdecide. -
permdock rls generatecompiles role checks to per-statement helpers. Policies callpermdock_has('<grant key>')for global roles and"<tenant col>" in (select permitted_tenant_ids('<grant key>'))(orpermitted_team_ids) for scoped roles:security definer,search_path = ''SQL functions over a seededrole_permissions (role, permission, grant_key, scope, effect)table, which Postgres evaluates once per statement (an InitPlan or hashed SubPlan) instead of once per row.--authorize database|jwtnow drives them for every dialect. Tenant grants with only a claim and global grants with no condition no longer skip the role check.Policies collapse to one permissive and one restrictive policy per table and command (
{table}_{op});--policy-per-rolekeeps the per-role layout and--policy-namesets a template. The tenant claim is cast to--tenant-type(defaultuuid). Top-leveldefinePolicy({ grants })compile too, and a grantee Postgres cannot see fails with the permission named.--rbac supabasebuilds on the same helpers;authorizeSqlreads the sharedrole_permissions.rls importreads both layouts back to roles andmemberOf,rls verifypasses roles and memberships in the claims and inserts whole rows, andrlsParitysets the role and memberships GUCs. -
permdock rls generatecompiles two more rules.deny(permission, { to: actor('oauth-client') })becomes a RESTRICTIVE policy that refuses rows while the token carriesclient_idoract, so a third-party app's Supabase token stops where the in-processdecidestops it.containson a column the resource schema types as an array compiles tovalue = any(column), cast to the item type, instead ofLIKEover the column's text. -
permdock rls generateemitsmember_<scope>_ids()for every declared scope: the instances the caller holds any live membership of, with no permission key, for policies such as an organization switcher or "members read their organization's row". Indatabasemode the helpers read thefromTable/fromJunctionsources in the newrls.membershipSources(defaultsupabase.hook.memberships) for every scoperls.membershipsmaps no table for, so the helpers, the token hook and the application'sMembershipSourcerun the same SQL.rls importreadscol in (select member_<scope>_ids())back as amemberOfwith no roles, and the hook manifest lists the new helpers. -
permdock rls generatefollows the Postgres and Splinter rules its docs cite:- One permissive policy per table, command and database role.
anyone()grants are ORed into theauthenticatedpolicy and get their ownTO anonpolicy, so Splinter'smultiple_permissive_policieslint stays quiet. - The
neondialect targets Neon'sanonymousrole instead ofanon. - The
gucdialect wrapscurrent_setting(...)in(select ...), so each setting is read once per statement. rls importreads the wrapped form back.
- One permissive policy per table, command and database role.
-
permdock rls generateon Supabase writesmember_<scope>_ids_for(p_user uuid)for each scope with a membership source (rls.membershipSourcesorsupabase.hook.memberships) or a mappedrls.membershipstable. It answers whatmember_<scope>_ids()answers, forp_user, from the membership tables, with the same expiry and suspension rules, so asupabase.hook.claimsfunction can read the user's scopes before a token exists.executeis revoked frompublic,anonandauthenticated. Withsupabase.hook.claimsset,permdock supabase hook generategrants it tosupabase_auth_admin(in the--grants-outfile when given), and the hook manifest and PD039 list themember_<scope>_idsandmember_<scope>_ids_forhelpers. -
permdock rls generatereadsrls.dialectfrom the config, aspermdock rls verifydoes;--dialectstill overrides it. It used to fall back tosupabasewhenever--dialectwas absent, so aneonorgucconfig silently produced Supabase SQL. An invalidrls.dialectnow exits 2 like an invalid flag. -
permdock rls generateskips every grant that requires an approval. It skipped onlyapproval: 'human', so anapproval: { by }grant compiled into a plain policy and the database allowed the action without the approval. -
permdock rls migrate --sql <dir>rewrites a project's own SQL helper calls increate policyandalter policystatements ontopermitted_<scope>_ids,permdock_hasandmember_<scope>_ids, as configured inrls.migrate(helper forms, exact key renames and prefixes). It is a dry run until--write, splices by parser location so formatting is kept, prints{ rewrites, skipped }with--json, and exits 1 while a mapped key is unknown. Calls it cannot rewrite safely (a computed id, a dynamic key, a key with row conditions or not seeded on that scope, a call inside a function body or outside a policy) are reported with their line and left as written.rls generate --helpers-only(orrls.helpersOnly) writes only the helpers, seeds and scaffold, andrls verify --introspectwith it diffs the helpers androle_permissionsseeds exactly, fails on a hand-written policy that passes an undeclared or row-conditioned key, and lists RLS tables that call no helper as info. -
permdock rls verify --introspect --db <url>compares the live database with whatrls generatewould write: policies (by name, command, permissive or restrictive, and roles), row level security on each table, theanonandauthenticatedgrants, and whether each helper is stillsecurity definerwithsearch_path = ''. It prints one line per difference and exits1, so a hand-written permissive policy or a missing grant shows up before a user finds it. -
permdock rls verify --treefills the required columns its generated rows leave out, so it runs on tables with aNOT NULLtenant column or name: a foreign-key column takes an existing value of the referenced table, any other column a placeholder of its type. Before, the seed failed on any such table. -
permdock supabase hook generategrantssupabase_auth_adminusage on the schema of every table it reads outside the hook's schema, so a schema-qualified source such asfromJunction({ table: 'better_supabase.memberships', … })works without a hand-written grant. -
permdock supabase hook generatewarns with PD039 when the helper schema has no generatedpermdock_hasorpermitted_<scope>_ids(read fromrls.outand the migration folders, or the database with the new--dbflag).permdock doctorPD039 reports the same and, with adoctor.claimsfixture of sample tokens, eachsupabase.hook.claimsentry larger than the memberships budget. -
permdock supabase inspect --outwith no path writespermdock.manifest.json, andinspect --checkwithout--outchecks that file instead of exiting 2. Thedatetime_preferencesclaim insupabaseClaimFixtures.fullnow uses CentraKit's keys:timezone,week_start,date_formatandtime_format. -
permdock.explain(permission, data)(ordecidewithexplain: true) returns the decision with atrace: how many grants were evaluated, the allows and denies that matched (trace.denies[0]is the deny that won) and the grants skipped with the reason.MatchedGrantcarries the grant'sname; adenydenial from a named deny carriesdetail: { name }.describePolicycells takedeniedBy. The trace is off by default and never emitted on a decision event. -
permdock/a2aemits Agent Cards in the A2A 1.0 wire form, and the cards validate against the publisheda2a.json. The card URL moves intosupportedInterfaces, security schemes use the one-of form (oauth2SecurityScheme), skill requirements become{ schemes: { name: { list } } }, and skills carrytags. The card also fillsdescription,capabilitiesand the default input and output modes.securitySchemeskeeps its OpenAPI-style input with a typedtype.sign(card, signPayload)now returns the card with a detached RFC 8785 JWS appended tosignatures, instead of{ card, signature }. -
permdock/authzenfollows more of AuthZEN 1.0:- Every response echoes
X-Request-ID. /access/v1/evaluationshonoursoptions.evaluations_semantic, and treats a request without anevaluationsarray as a single evaluation.- Discovery under
/.well-known/authzen-configuration/<path>names the path-qualified PDP identifier. - Search accepts
page.limitand returnspage.countandpage.total. - Subject search answers only for the
usersubject type.
- Every response echoes
-
permdock/evefollows eve 0.70:approval.responsereads the responder fromctx.response.principal, and a Cancel from an eligible responder records the approval asrejected, so the re-check denies.permdock/drizzleaccepts drizzle-orm 1.0 release candidates next to 0.40 and later, and recognises 1.0 array columns (dimensions) forcontains. The@supabase/middlewarepeer range is now>=1. -
permdock/jwtandpermdock/supabasenow take the outermost RFC 8693act.subas the actor. RFC 8693 section 4.1 makes that the current actor and says prior actors in nestedactclaims must not decide access; before, the innermost (oldest) actor was used. Every level of the chain must carry a non-empty stringsub, or the subject is anonymous with causeinvalid-chain. The full chain stays ondelegation.chain. -
permdock/jwtfollows OpenID Connect more closely. Discovery never fetches anhttp:jwks_uri, and an inlinemetadata.jwks_urioverhttp:is a configuration error. Withaccept: 'id-token', an ID token needsiat, needsazpwhen it has several audiences, and itsazpmust be the client id whenever present. The OpenID Connect Core section 5.1 profile claims (email,name,pictureand the rest) no longer reachprincipal.claims.claims.membershipsnow flattens Zitadel'srole -> { orgId: domain }project roles into one membership per organisation. -
permdock/mcpfollows the 2026-07-28 specification more closely. A missing scope is challenged with the scope the call needs plusresource_metadata, not the held scopes plus the missing one;permdock/a2aand HTTP challenges use the same builder.approval-requiredbecomes a multi-round-tripinput_requiredresult with a URL-mode elicitation and the approval token asrequestStatewhenapproval.atis set and the client declares URL elicitation, and theelicitationhint is gone from MCP and WebMCP refusals.stepUp: { at }does the same forinsufficient-user-authentication, withacr_valuesandmax_agefrom the failingassurancegrants, which a denial now carries into. Aresourceoption refuses tokens issued for another RFC 8707 audience.ActionMetagainsdestructiveandidempotent; tool annotations the author leaves out are filled from the permission, andcrud()marksdeletewithdestructive: trueinstead oftags: ['destructive']. The unusedInsufficientScopeErrorexport is removed. -
permdock/mcp:createPermDockandsubjectFromMcpnow read RFC 9396 authorization details from bothauthInfo.extra.authorizationDetailsand the raw claim nameauthorization_details. Before, each read only one of the two, so the same verifier output lost its details in one of them. -
permdock/nexttargets Next.js 16.3 (thenextpeer range is now>=16.3).createPermDockalso returnsrequireAccess({ permission, data?, tenant? }), which resolves to the grantedDecisionand interrupts a denial withunauthorized()for an anonymous subject orforbidden()otherwise (experimental.authInterrupts); approval-required and boundary validation still throwPermDockApprovalRequiredErrorandPermDockValidationError. The request-scoped instance is created afterawait io(), so decisions that read the clock no longer trip the "Date.now()while prerendering" error under Cache Components, and never block a prefetch the wayconnection()would. Aredirect(),notFound()or other Next.js interrupt thrown by thesubjectortenantresolver now propagates (unstable_rethrow) instead of turning the request anonymous. Sink writes and oneflush()per render run insideafter(), so a serverless function stays alive until buffered decision events are delivered. -
permdock/openapimarks an operation withx-permdock-approval(and the optionalx-badgeshint) for every grant that requires an approval. It marked onlyapproval: 'human', soapproval: { by },{ distinct: false }and{ staleOn }grants looked approval-free in the description. -
permdock/supabaseexportsactorOf(claims)anddelegationOf(claims), the rulessubjectFromSupabaseuses for the acting app:actorOfreturns{ ok: true, actor? }with the innermostactsub(orclient_id) and a frozen copy of the chain, or{ ok: false, reason: 'invalid-chain' }, which must deny;delegationOfreturns thescopeclaim as{ scopes }.supabaseClaimFixturesentries now state the expectedactoranddelegation. -
permdock/supabaseexportssupabaseClaims({ tenantClaim? }), a Standard Schema v1 object for the hook's claim contract, and its typesSupabaseClaimsandSupabaseMembershipClaim;.extend(appSchema)merges a Zod, valibot or other Standard Schema over it, so a session library validates tokens without copying the shape.subjectFromSupabasenow readsgrantedByandreasonon memberships,supabase-claims-v1.jsoncoversapp_metadata,client_id,scopeandact, andsupabaseClaimFixturesaddsfull,portalContact,oauthClientandactChain. -
permdock/supabaseexportssupabaseTenantClaim(tenant_id), the one default the hook,subjectFromSupabase,supabaseRls,rls generate,rls verifyandrlsParityshare.permdock doctorPD038 warns when asubjectFromSupabaseorsubjectFromSupabaseSessioncall reads the tenant from a different claim thanrls.tenantClaim. -
permdock/supabasereadsact.kindas better-supabase 0.5.1 writes it:supportbecomes actor kindsupportwithsessionIdandreadOnly, andimpersonationbecomes actor kindimpersonation. Neither carries a delegation, so they reach only what a policy delegation names. An actor withreadOnly: truegets only the read-only permissions of each policy delegation (readOnlyonpolicy.delegations); other permissions deny withnot-delegated, andmayUsefollows the policy delegation ceiling. An unknownkind, or a support level withoutsession_id, is the anonymous subject.supabaseClaims()andsupabase-claims-v1.jsonvalidate the newactfields. -
permdock/terminalasks for the resource id to be typed before running adestructivepermission (replace the prompt withinteractive: { typed }), and without a terminal exits with the newEX_USAGE(64) unless--yesor-yis passed or theyesoption is set.--dry-run(ordryRun: true) decides as a simulation, prints the decision and exits with its code without running the action or consuming a limit. Theci-oidctoken source takes{ source: 'ci-oidc', audience }to request a GitHub Actions audience and{ source: 'ci-oidc', env }to read a GitLabid_tokensvariable.permdock/jwtaddssubjectFromCiOidc(token, { provider, audience, issuer?, schema? })for GitHub Actions, GitLab CI and Buildkite tokens, returning aworkloadprincipal withrepository,refandenvironment, never a user. -
permdock/terminal: a network failure during the device flow now ends thedevicetoken source with no credential, so the chain moves on to the next source, the same as a failed refresh or revocation. Before, the thrownfetcherror escaped the command. -
permdock/testing/saasadds scenarios for an agent acting without a delegation, a delegation narrower than the user's grants, an exhausted api-key quota, a team role held on the tenant membership,org-1againsttenant-1on lists and writes, and an expired membership on every permission class. Scenarios may carryactor,delegationandquotaUsed;saasUser,saasScenarioOptionsandsaasLimitStorebuild the subject and options a case runs with. -
permdock/webmcpnow validates instance-tool arguments against the resource schema whenregisterToolsis given a resource node such aspermissions.post, or a nested group, as the docs show. Before, the schema was found only from the root tree, so agent input went unvalidated. Collection actions no longer pick up the resource schema, and an explicitschemaoption now overrides it. -
receiver.pollreports polled SETs that can never verify under RFC 8936setErrsin the acknowledgement request, so a transmitter stops redelivering them. A SET whose handler failed is still left pending for redelivery. -
resource()takesnamefor a resource name apart from its path, action metadata takesmeta.xfor application-owned JSON,rls.actionsmaps action verbs such asviewto SQL commands (or'none'), anddescribe(decision, { messages })localises titles and details. -
scimHandleranddirectoryMembershipSourcereadgroupRolesby own key only. A group id such asconstructoror__proto__no longer resolves to anObject.prototypemember, which made the SCIM endpoint answer 500. -
sender: 'dpop'(theprofile: 'fapi2'default) now resolves the anonymous subject withdpop-proof-invalidwhen noRequestis passed, instead of skipping the proof.verifyDpopProofrefuses a symmetricalgand ajwkheader carrying private key members, per RFC 9449 section 4.3. -
simulate()no longer throws on an Arazzo step whoseparametersarray holdsnullor a non-object. It skips those items and decides the step as usual. -
snapshotTag(sub)frompermdock/nextreturns the cache tag for a user's snapshot entries (permdock:<sub>, orpermdock:anonwithout one), so a private-cache loader and the Server Action that changes roles agree on one string. The Next.js Cache Components guide gains the better-supabase 0.4 recipe:bs.cached({ tags })inside'use cache: private',stalecapped by both the session andcacheLifeFor(snapshot), andbs.invalidateSession(sub, { tags: [snapshotTag(sub)] })after a role change. Its URL-slug loader now takes the org id and never callsnotFound()inside the private cache. The newapps/examples/next-better-supabaseruns that recipe in snapshot-only mode against Postgres in testcontainers.permdock doctorPD014 now accepts a shorthandissuerproperty next tojwks. -
subjectFromSupabaseandsubjectFromSupabaseSessiontakeanonymousSignIns: 'deny', which maps asignInAnonymously()token (is_anonymous: true) to the anonymous subject, asrls.anonymousSignIns: 'deny'does in RLS.permdock doctorPD057 warns whenrls.anonymousSignInsis'deny'and a subject call does not pass the option. -
subjectFromSupabasemaps more of the token.plans: '<claim>'reads the active tenant's entry of a per-tenant plans claim (better-supabase'sfeatures) intoprincipal.plans.actand the OAuthclient_idof a third-party app become anoauth-clientactor withscopeasdelegation.scopes, so such a token only reaches what its scopes delegate. Amembershipsentry the reader cannot use is still dropped, and now reportsmembership-droppedto the newonAuthoption;permdock doctorPD039 lists those entries from thedoctor.claimssamples.supabaseClaimFixtures.betterSupabaseis the canonical claim set better-supabase 0.4 emits. -
supabase.hook.claimsadds claims other packages own to the generated Custom Access Token Hook, for example{ features: 'better_supabase.feature_claims' }. Each value is a schema-qualified(uuid) returns jsonbfunction;nullleaves the claim out.permdock supabase hook generaterefuses a claim name PermDock or Supabase Auth writes (roles,memberships, the tenant claim,sub,roleand the rest) and an unqualified function, grantssupabase_auth_adminexecuteon each function, keeps the claims outside the memberships budget, and writes none of them for a suspended user. -
supabaseClaimFixturesinpermdock/testingaddssupportSession,supportSessionReadOnly,impersonationandanonymousSignIn, and each fixture states the actor and delegation it maps to. -
supportAccess()now type-checks insidedefinePolicy({ roles })when the policy's scopes are literal: it returnsRoleBinding<S>foron: SandRoleBinding<'tenant'>withouton, the scope it defaults to. -
webBotAuthtakes anowclock in Unix seconds for thecreatedandexpireschecks, so the RFC 9421 Appendix B vectors and replayed captures can be verified.