Authorization landscape (September 2026)
Survey of TypeScript and JavaScript authorization libraries, policy engines (Cedar and Amazon Verified Permissions, Open Policy Agent, the Zanzibar family, Casbin), auth-provider access control, schema-embedded policies and MCP tooling, with a gap matrix that motivates PermDock.
Source: survey conducted 2026-09-06. Star counts and last-commit dates are shields.io badge values fetched that day; versions are from npm view on the same day; bundle sizes for permix and CASL are permix's own measurements (2026-06-02), other sizes are npm unpackedSize. The Cedar, Amazon Verified Permissions, Open Policy Agent, Auth0 FGA and WorkOS FGA entries, and the extended OpenFGA, SpiceDB, casbin and accesscontrol entries, were read from the vendors' official docs, repositories and npm pages on the same day. Anything that could not be confirmed is marked unverified. Companion deep-dives: permix, CASL v7, Kilpi v1, zap-studio/permit.
Executive takeaways
- Two families dominate: in-process TypeScript libraries with declared permission definitions (CASL, permix, Kilpi, accesscontrol, Better Auth's
createAccessControl), and external PDPs with strings-in, boolean-out SDKs (Cerbos, Permit.io, OpenFGA, SpiceDB, Oso Cloud, casbin). The second family has near-zero TypeScript inference; the first has good key inference but weak or no data-layer integration (CASL is the exception). - Standard Schema is essentially unused in authorization. Only
@zap-studio/permitbuilds resource definitions on~standard, despite 30+ validators implementing the spec and Zod and ArkType also implementing Standard JSON Schema. - Nobody emits OpenAPI
securityor scopes from permissions. Every generator (@hono/zod-openapi,hono-openapi,@orpc/openapi,trpc-to-openapi,zod-to-openapi, next-openapi-gen) requires hand-attachingsecurityper route, and nothing downstream (Hey API, Orval, Scalar) can know what a route enforces unless the description says so. - MCP per-tool authorization is a hand-written scope check inside tool handlers in the official SDK. The products in this space are proxies (Permit MCP Gateway), ReBAC guides (OpenFGA), agent monitoring (Oso for Agents) or Python-first tool platforms (Arcade). No TypeScript library exposes a declarative permission-to-tool binding.
- permix is the closest thing to a modern all-framework library, but it has no conditions-to-SQL, no explain, no Standard Schema, no OpenAPI, no React Native docs.
- CASL remains the only mature in-process library that turns conditions into Prisma and Mongoose
whereclauses and does field-level permissions. v7 (May 2026) modernised it, but typing is declared, not inferred, and there is nollms.txt. - Explain-why-denied is universally weak. CASL has
reasonon rules andrelevantRuleFor; the PDPs have audit logs and policy tests; Kilpi has an audit plugin; nobody returns a structured decision trace by default. - TypeScript 7 (Go-native) is shipping:
typescript@7.0.2was published 2026-09-06. The compile-time budget for heavy generics got roomier but instantiation and recursion limits are unchanged. The disruption is the missing stable compiler API in 7.0 (planned for 7.1), which affects codegen tools, not runtime libraries.
In-process TypeScript libraries
CASL
@casl/ability 7.0.1, @casl/react 7.0.1, @casl/prisma 2.0.2, @casl/mongoose 9.0.2. MIT, about 7.1k stars, last commit the week of the survey.
- Model: rules are
actionplussubjectplus optional MongoDB-styleconditions(via@ucast) plusfields(glob patterns) plusinverted. Isomorphic. - Typing:
AbilityBuilder<MongoAbility<[Actions, Subjects]>>; the action and subject tuple is declared, not inferred from a definition object. Subject types come from classes orsubject('Post', obj); object detection needsdetectSubjectType. - Adapters:
@casl/react(Can,useAbility),@casl/vue,@casl/angular,@casl/aurelia. No Next.js or RSC-specific code, no Svelte or Solid, no server middlewares. - Runtime:
packRules/unpackRulesfor compact serialisation (SSR hydration is do-it-yourself),ability.update(rules)withon('updated'),ForbiddenError.from(ability).throwUnlessCan(...),reasonon rules,relevantRuleFor(). - Data layer:
accessibleBy(ability, 'read').ofType('Post')returns a PrismaWhereInput; v2 no longer throws on no rules, you addcreateCaslExtensionso empty conditions fail closed (Prisma 4 to 7; Prisma 7 needs the@casl/prisma/runtimewrapper).rulesToQuerywas replaced byrulesToConditionin v7;rulesToASTfor custom SQL. - None of Standard Schema, Zod, OpenAPI;
casl.js.org/llms.txtreturns 404. Dual CJS/ESM, about 6.17 kB min+gzip core. - v7 breaking changes (2026-05-21):
PureAbilityrenamedAbility,rulesToQueryrenamedrulesToCondition,readonly Rule[], empty conditions mean match-all. - Sources: repo, ability 7.0.0 release, prisma 2.0.0 release, @casl/prisma on npm.
permix
permix 4.1.2. MIT, 617 stars, ESM-only, Node 22+.
- Model: resource by action booleans, optionally data-typed so rules can be closures over the entity; RBAC, ABAC and "ReBAC patterns" via closures. No condition AST.
- Typing:
createPermix<{ post: ['create','read'] }>();check('post.create')keys are template-literal unions;.contextKey('permissions')literal inference in the tRPC adapter; the Drizzle adapter derives resource keys from schema export names. - Adapters, all subpaths of one package: React, Vue, Solid, Svelte, Next.js App Router (per-request instance via React
cache()), TanStack Start, Node,permix/server, Express, Hono, Fastify, Elysia, tRPC, oRPC, Effect, Drizzle. No NestJS, Nuxt, Astro, Remix or React Native docs. - Runtime:
setup(),check(),template(),hook(),isReady,dehydrate()/hydrate()(function rules cannot serialise). No route guards, no optimistic UI, no caching beyond Nextcache(). - No validation, no OpenAPI, no explain.
llms.txtandllms-full.txtserved; agent skills mentioned. Core 2.64 kB gzip; adapters 0.8 to 2.2 kB. - Sources: repo, comparison, Next.js, Drizzle, hydration.
Kilpi
@kilpi/core 1.1.3, @kilpi/client 1.1.0, @kilpi/react-server 1.0.4. MIT, 89 stars, ESM, last commit December 2025.
- Model: policies as TypeScript functions returning
Grant(subject)orDeny(), subject resolved bygetSubject(); server-first. - Typing: subject narrowing after grant, resource parameter types, policy keys inferred from the
policiesobject. No Standard Schema (Zod internally). - Adapters: RSC plugin (
Authorizecomponent),@kilpi/client(fetch decisions with caching and batching),@kilpi/react-client. Next.js and Hono examples; no Vue, Svelte, Solid, Express or tRPC packages. - Runtime:
authorize()andassert(), protected queries that co-locate authorisation with data fetching and redact fields,onUnauthorizedAssert,onAfterAuthorization,AuditPluginwith batch, periodical, manual and immediate strategies andwaitUntilfor serverless. - No data layer (redaction is post-fetch), no OpenAPI,
llms.txtreturns 404. Nine months without commits. - Sources: repo, audit plugin.
accesscontrol
accesscontrol 3.1.0 (published 2026-07-16). MIT, 2.3k stars, last commit July 2026, ESM, Node 20+; two pinned runtime dependencies (notation, dtrexp).
- Model: chainable grants per role,
ac.grant('user').createOwn('video'),ac.can('admin').updateAny('video')returninggrantedandattributes; CRUD helpers are sugar over.action()/.do()so custom actions such aspublish:ownwork;.extend()inheritance with deny-overrides; possessionanyversusown, enforced whenpolicy.ownerFieldor a resolver is configured. - v3 (2026 rewrite) policy engine:
.where('$.order.value <= 100000')conditions with operators==,in,contains,matches(opt-in regex),before/after/during(dtrexp schedules),cidr, combinable withand/or/not, stored as canonical JSON;require()gates at global, category or resource scope that can only restrict and fail closed on missing context; groups and categories viasetup();defineConditionplusgrantedAsyncfor async custom checks. - Runtime: glob attribute lists (
['*', '!password', 'profile.*']) andfilter()for field-level output;tryCan()never throws;access,changeanderrorevents;snapshot()/restore()andgetGrantsList()for database persistence;lock(); prototype-pollution-safe name handling; strict mode for unknown roles and actions;nestjs-accesscontrolas the official framework integration, Express shown as an example. - Roles, resources and actions are runtime strings with no inference; conditions evaluate in memory only (no
whereclause or RLS); no client snapshot, no UI helpers, no OpenAPI, MCP or AuthZEN, nollms.txt. - Sources: repo, npm.
Smaller and unmaintained
@rbac/rbac2.2.2 (published 2026-08-28): small RBAC with role inheritance and asynccan().role-acl4.5.4 andpermissio2.0.2 (LGPL-3.0): unmaintained since 2022.@typed-policy/core0.4.0 (MIT, 0 stars, February 2026): one policy AST (eq,neq,inArray,gt,lt,exists) withevaluate()on the client andcompileToDrizzle()for awherefragment. Conceptually the "one definition, SQL and UI" idea; tiny and unproven.@permx/core0.4.0 (MIT, 0 stars, April 2026): structured RBAC stored in Mongoose or Prisma, Express middleware, headless React SDK (Can,CanField,RouteGuard,FeatureGate).permissionize,zod-permissions,access-control-ts,@authz/core,casl-prismado not exist on npm.
@zap-studio/permit
@zap-studio/permit 2.0.1 (published 2026-09-01). MIT, monorepo with 171 stars, ESM, Node 18+. The only authorization library built on Standard Schema: createPolicy({ resource: schema, actions }), allow, deny, when, role hierarchies, policy merging; resource types inferred from any Standard Schema validator. Framework-agnostic with no shipped adapters; structured errors, optional logging, OpenTelemetry hooks; no hydration, data layer or OpenAPI. zapstudio.dev/llms.txt serves. Sources: monorepo, npm. Detail in zap-studio/permit.
External policy engines with JavaScript SDKs
Cerbos
@cerbos/core 0.33.0, @cerbos/http 0.30.0, @cerbos/grpc, @cerbos/embedded 0.15.4 (WASM), @cerbos/react 0.5.0, @cerbos/hub. Apache-2.0; cerbos 4.6k stars, JS SDK 83 stars; ESM, Node 22+. Policy-as-code in YAML with CEL conditions evaluated by a PDP (sidecar, hub or embedded WASM). SDK typed on the protocol (checkResource({ principal, resource, actions })), resource kinds and actions are strings, no typegen from policies found (unverified). ORM adapters via query plan (planResources to Drizzle, Prisma, Mongoose), OpenTelemetry, audit logs, YAML policy tests, AuthZEN support. docs.cerbos.dev/llms.txt serves. Sources: JS SDK, docs.
Permit.io
permitio 2.7.6. MIT; permit-node 46 stars, OPAL 5.5k stars; CommonJS, 2.6 MB unpacked. RBAC, ABAC and ReBAC via an OPA/OPAL-backed PDP; permit.check(user, action, resource) with strings. No adapters. The Permit MCP Gateway is a proxy between agents and MCP servers that classifies tools into trust levels by name, enforces at call_tool and returns everything from list_tools; enforcement is external to your code. docs.permit.io/llms.txt returns 404. Sources: permit-node, docs.
OpenFGA
@openfga/sdk 0.9.7. Apache-2.0; openfga 5.7k stars, js-sdk 88 stars; CJS and ESM, Node 20.19+. Owned by the Cloud Native Computing Foundation.
- Model: Zanzibar ReBAC. A model DSL (
model,schema 1.1,type documentwithrelationssuch asdefine viewer: [user], usersets likeorganization#member, object-to-object relations for parent folders) plus relationship tuples written through the API. RBAC is modelled as relations on objects; ABAC through conditions (Google CEL expressions with typed parameters attached to tuples, evaluated against requestcontext, default evaluation cost cap of 100) and contextual tuples. - API:
Check,BatchCheck(the docs suggest parallelCheckunder ten items),Read(stored tuples only, no graph traversal),Expand(userset tree for one relation),ListObjects(objects of one type a user has a relation with, positioned for access-aware filtering of small collections) andListUsers. HTTP and gRPC; SDKs for JavaScript, Go, Java, .NET and Python; Postgres, MySQL or SQLite as production datastores; CLI, VS Code extension, GitHub Actions, Helm charts, OpenTelemetry. - OpenAPI-generated client, no types derived from your model (unverified).
ListObjectsreturns ids to feedWHERE id IN (...), not a where-clause compiler. - Agents: the use cases section covers AI agent authorization, RAG filtering and MCP server authorization: a
tooltype withdefine can_call: [user:*, user, role#assignee], acheckon every request and list-objects to filter the tool list; agents are modelled as their own principals for independent scoping and revocation. openfga.dev/llms.txtserves. Sources: js-sdk, introduction, modeling, relationship queries.
Auth0 FGA (Okta FGA)
The managed service built on OpenFGA. Auth0's product page states in its FAQ that "Auth0 FGA is built on OpenFGA", and Auth0 Lab records the Sandcastle experiment graduating as "Okta FGA" while its core was published as OpenFGA, so the two names refer to one service. Documentation at docs.fga.dev follows the OpenFGA shape: define an authorization model, write relationship tuples, check permissions from your API with the SDK. Vendor claims of a 99.99 percent availability SLA, multi-region deployment and a logging API for an audit trail are the vendor's own and were not independently checked. Positioning is explicitly about RAG, MCP servers and agents as principals. Same limits as OpenFGA for PermDock's purposes: strings in and booleans out, no types from your model, filtering via list-objects ids. Sources: product page, docs, Auth0 Lab.
SpiceDB / AuthZed
@authzed/authzed-node 1.6.1. Apache-2.0, spicedb 7k stars, ESM, 5.6 MB unpacked (gRPC and protobuf), published October 2025.
- Model: Zanzibar ReBAC with a
.zedschema language:definitionper object type,relationwith allowed subject types (user,group#member, wildcardsuser:*),permissionas computed sets with union (+), intersection (&), exclusion (-) and arrows (parent_folder->read,.any,.all); caveats are CEL expressions (caveat ip_allowlist(...)) that make a relationship conditional at check time; relationships can expire. - API:
CheckPermission,CheckBulkPermissions,LookupResources,LookupSubjects,WriteRelationships,ReadRelationships, a Watch API, ZedTokens for consistency. The list-endpoint guide names the three filtering strategies:LookupResourcesids intoWHERE id = ANY(ARRAY[...])when the accessible set is small (the docs suggest under 10,000),CheckBulkPermissionsover pages of candidates otherwise, and Materialize (early access) for a denormalised local permission set, AuthZed's version of Zanzibar's Leopard cache. - Agents: an AuthZed MCP server, a SpiceDB Dev MCP server and LangChain / LangGraph and RAG integrations are documented; the "Coming from Open Policy Agent" guide targets OPA users.
- Strings in and out, no adapters, no types from the schema.
authzed.com/llms.txtserves. Sources: authzed-node, schema language, protecting a list endpoint.
WorkOS FGA
Hosted, part of the WorkOS platform (RBAC, SSO, Directory Sync, AuthKit). Frequently grouped with the Zanzibar family, but its current documentation describes a hierarchical, resource-scoped RBAC and WorkOS says so explicitly: "no confusing schema DSL", "no DSLs to learn".
- Model: resource types are configured in the WorkOS Dashboard; resources are registered at runtime with a type, an id and a parent; roles and permissions (
workspace:edit,project:delete) are scoped to a resource type and a role may include permissions for child types, so oneworkspace-adminassignment propagates down to projects and apps; assignments bind a subject to a role on a resource. Subjects today are organization memberships and groups. A permission's or role's resource-type scope and slug are immutable after creation. - API:
check(one permission on one resource),listEffectivePermissions(all permissions on one resource),listResourcesForMembership(one permission across resources, for list views),listMembershipsForResource(who has access);@workos-inc/nodeexposesworkos.authorization.check({ organizationMembershipId, permissionSlug, resourceExternalId, resourceTypeSlug })returningauthorized. Organization-scoped roles and permissions are embedded in AuthKit access tokens so org-wide checks are JWT-only; resource-scoped checks call the API. Vendor performance figures (sub-50 ms p95, strong consistency) are the vendor's own. - No conditions or attribute rules in the docs consulted, no policy language, no client snapshot, no types from the model; enforcement is a network call per resource-scoped check. Sources: overview, roles and permissions, access checks, API reference, blog.
Oso
oso 0.27.3 is deprecated (Apache-2.0, 3.5k stars, last commit February 2025); oso-cloud 2.6.0 (March 2026). Polar language over facts; authorize, list, and listLocal / authorizeLocal return SQL fragments to filter your own database. "Oso for Agents" is discovery, monitoring and an edge proxy for agent traffic and MCP connectors, a security product rather than a library. osohq.com/docs/llms.txt serves. Sources: oso, docs.
casbin
casbin 5.51.1 (published 2026-06-25). Apache-2.0, node-casbin 2.9k stars, moved to apache/casbin-node-casbin under the Apache incubator; CJS, about 510 kB; runtime dependencies @casbin/expression-eval, await-lock, buffer, csv-parse, minimatch.
- Model: the PERM metamodel (Policy, Effect, Request, Matchers) in a
.conffile.r = sub, obj, actdefines the request tuple,p = sub, obj, act, eftthe policy shape,m = r.sub == p.sub && r.obj == p.obj && r.act == p.actthe matcher, and the effect expression combines matches:e = some(where (p.eft == allow))allows if any policy allows,e = some(where (p.eft == allow)) && !some(where (p.eft == deny))makes deny override. Policies live in CSV (p, alice, data1, read) or database adapters. - RBAC:
[role_definition]withg = _, _(andg2for resource roles);g, alice, data2_admingrouping policies; matcherg(r.sub, p.sub); transitive hierarchy with a default maximum depth of 10; RBAC with domains and tenants as a documented model. ABAC: pass objects intoenforceand reference fields in the matcher (m = r.sub == r.obj.Owner), or store rules in the policy and evaluate them witheval(p.sub_rule). Eighteen documented model families including RESTful path matching (keyMatch), IP match, priority (first match wins) and deny-override. - API (node-casbin):
newEnforcer(model, policy),enforce(sub, obj, act)returning a boolean,enforceSync, a Management API and an RBAC API (getRolesForUser), adapters for persistence, watchers for multi-node consistency, Batch API. Theinmatcher operator is not yet available in Node-Casbin. Casbin's own docs note that a Watcher or Role Manager checkmark means the interface exists, not that an implementation ships for every language. - No TypeScript inference (all names are strings), no framework adapters, no data-layer compilation, no client snapshot, no explain beyond the boolean, no
llms.txt. Multi-language parity (Go, Java, Node.js, PHP, Python, .NET, Rust and more) is the distinctive strength. - Sources: repo, npm, overview, how it works, supported models, RBAC, ABAC.
Cedar and Amazon Verified Permissions
Cedar: Apache-2.0 policy language and Rust engine from AWS; cedar-policy/cedar about 1.7k stars (GitHub, fetched 2026-09-06). @cedar-policy/cedar-wasm 4.12.0 (published 2026-07-28) ships WASM bindings with TypeScript types for the engine's functions, in ESM, CommonJS (/nodejs) and web (initSync) flavours; the reference guide documents Cedar 4.5 and Amazon Verified Permissions states it currently uses Cedar 4.7.
- Model: policies are
permitorforbidwith a mandatoryprincipal,action,resourcescope (==,in,is) and optionalwhen/unlessconditions over attributes andcontext; annotations (@id,@advice) carry metadata without affecting evaluation. Combining rule: at least onepermitand zeroforbidmeans allow; anyforbidor nopermitmeans deny (implicit deny, explicit deny overrides). - Schema: entity types with attributes, optionality and parent (membership) relations; actions with
appliesToprincipal and resource types and acontextshape; common types; namespaces; enumerated entity types since 4.3. Cedar does not use the schema at evaluation time; the validator type-checks policies against it when they are authored, and thecedar-policy-symcccrate is a symbolic compiler that verifies properties about policy sets with concrete counterexamples. Entities and their attributes are supplied per request as a slice. - Amazon Verified Permissions: hosted policy stores (one per application or tenant, optional schema, policy validation rejects invalid policies, policy templates, aliases, namespaces),
IsAuthorized,BatchIsAuthorized(up to 30 requests sharing a principal or resource, up to 100 principals and 100 resources inentities) andIsAuthorizedWithToken(principal from a Cognito or OIDC ID or access token; principal attributes come only from the token), responsesALLOWorDENYwithdeterminingPoliciesanderrors; console, CLI, SDKs, CDK constructs; an Express middleware integration. Pricing is tiered by authorization requests per month. - No inference from an application's TypeScript types (entity literals such as
User::"alice"are strings at the call site), no compilation of conditions towhereclauses or RLS in the docs consulted, no approval outcome, no AuthZEN mention found (unverified). - Sources: Cedar reference, policy syntax, schema, repo,
@cedar-policy/cedar-wasm, what is AVP, policy stores,IsAuthorized,BatchIsAuthorized,IsAuthorizedWithToken.
Open Policy Agent
Apache-2.0, CNCF graduated. General-purpose policy engine: Rego policies over arbitrary JSON input and data, used for microservices, Kubernetes admission control, CI/CD, API gateways and Envoy external authorization. @open-policy-agent/opa-wasm 1.10.0 (published 2024-11-08; the README calls it work in progress) loads opa build -t wasm bundles with loadPolicy, setData and evaluate, in CommonJS, ESM and browser builds.
- Model: Rego rules are if-then statements with a head and a body, complete or partial, with implicit iteration and OR expressed as multiple rules of the same name; domain-agnostic, so deny-override and the shape of principals and resources are conventions of the policy author.
- Deployment: daemon or sidecar with a REST API (
GET/POST /v1/data/<path>returningresultand, with decision logging,decision_id; Policy, Data, Query, Compile and Health APIs), Go library (rego.New,PrepareForEval), or WASM. The Compile API performs partial evaluation and can emit data filters for a target dialect (ucast+prisma,ucast+linq,sql+postgresql,sql+mysql,sql+sqlserver), the closest thing among external engines to a where-clause compiler. - Agents:
@ai-sdk/policy-opa(Vercel) evaluates Rego in AI SDK tool approvals via WASM or HTTP and fails open on unrecognised decisions (vercel/ai#19978); see comparison. - No inference from an application's types, JSON in and JSON out, a separate policy build and deployment pipeline, no approval outcome, no RLS output; AuthZEN support not found in the docs consulted (unverified).
- Sources: introduction, REST API,
@open-policy-agent/opa-wasm.
Authorization bundled with auth providers
Better Auth
better-auth 1.7.3 (MIT, 30k stars, ESM). createAccessControl(statement) with statement = { resource: ['action', ...] } as const, then ac.newRole({...}); used by the organization and admin plugins. Pure RBAC without conditions; "dynamic access control" stores roles in the database at runtime. Resource and action keys are inferred from the as const statement. Checks: server auth.api.hasPermission, client authClient.organization.hasPermission (network) and checkRolePermission (sync, excludes dynamic roles). No Can-style UI helpers, no hydration API, no data layer. better-auth.com/llms.txt serves. PermDock layers on top rather than competing (better-auth adapter). Sources: organization plugin, admin plugin.
Clerk
Org-scoped custom permissions (org:invoices:create), roles, features and plans. has() returns a boolean, protect() throws or redirects, Protect renders conditionally. Server-side has() only for custom permissions; system permissions need role checks. Type safety via manual ClerkAuthorization interface augmentation. Works in Next.js and Expo (Expo SDK existence verified, has() parity not). Vendor-locked, no conditions, no data layer. clerk.com/llms.txt serves. Source: authorization checks.
Auth.js / NextAuth
28k stars. No authorization primitives: augment Session and JWT types, copy role in jwt() / session() callbacks, gate in the authorized() middleware callback or auth() in RSC. Sources: discussion #9609, RBAC blog.
Policy in the schema or data layer
ZenStack v3
@zenstackhq/orm 3.9.3, @zenstackhq/plugin-policy 3.9.3 (MIT, 2.9k stars, ESM, published 2026-09-01). ZModel (Prisma-compatible schema) with @@allow / @@deny on models and @allow / @deny on fields, auth() for the current user, deny wins. Policies compile into SQL through Kysely so reads are filtered and writes rejected at the query layer. The ORM client is fully typed from the schema; policies themselves are DSL. v3 removed the DB-free check() permission checker that v2 had (SAT-based, with useCheckPost hooks), so UI-side "can I?" checks are gone. Auto CRUD API and TanStack Query hooks; Zod generation. zenstack.dev/llms.txt returns 404. Sources: write policies, field-level, migrate v2, breaking changes.
Keel and Nile
Keel: @permission(expression, roles, actions) on models and actions, expressions over the record and ctx.identity, multiple permissions ORed, secure by default. The teamkeel/keel repo returned "not found" from shields, so maintenance status is unverified. Nile: tenant isolation via virtualised Postgres, nile.withContext({ tenantId }) scopes all queries; row-level tenancy, not a permissions library (1.1k stars, active).
Payload, Directus, Strapi
- Payload (45k stars): access functions per collection operation return
boolean | Where(row-level via a query constraint); field-level access is boolean only. Ships a Claude plugin with skills, including an access-control reference, a notable example of agent-first docs. - Directus (38k stars): v11 policies produce permissions with
action,collection,permissions: Filter,validation: Filter,presets,fields: string[]; fully data-driven. Docs. - Strapi (73k stars): admin RBAC with per-field toggles; custom conditions are handlers returning a boolean or a sift.js query object registered via
conditionProvider.register(). Docs.
MCP and agent tool authorization
- MCP spec: OAuth 2.1 with PKCE; servers are resource servers; clients must implement RFC 9728 (Protected Resource Metadata) and RFC 8707 (resource indicators); servers should return
WWW-Authenticatewithscopehints;insufficient_scopeenables step-up. Scopes are considered too coarse for per-tool control and the spec leaves per-tool authorization to the server. Spec, llms.txt. - Official TypeScript SDK: v1
@modelcontextprotocol/sdk1.30.0; v2@modelcontextprotocol/server2.0.0 (published 2026-07-28, ESM, Node 20+).requireBearerAuthattachesAuthInfo(scopes,clientId,expiresAt) toctx.http.authInfo; per-tool checks are hand-written in handlers returningisError: true. ThescopeChallengeoption onregisterTool(PR #1624) is the one declarative hook and is whatpermdock/mcpbuilds on. - Permit MCP Gateway (proxy), OpenFGA MCP guide (ReBAC over tools), Oso for Agents (monitoring and proxy), Arcade (
@tool(requires_auth=...)decorator, Python-first, TS parity unverified), Composio (connected_account_idper toolkit). None is a TypeScript library you embed in your own MCP server. - Cerbos discusses AuthZEN as the PDP-to-PEP standard relevant to MCP (blog).
Cross-cutting specifications
Standard Schema
@standard-schema/spec 1.1.0 (December 2025, 3.6k stars). Three interfaces under one ~standard key: StandardTypedV1 (version: 1, vendor, types), StandardSchemaV1 (adds validate(value, options?) returning a value or issues), StandardJSONSchemaV1 (adds jsonSchema.input(opts) / .output(opts) with target of draft-2020-12, draft-07, openapi-3.0 or a string). Type extraction via StandardSchemaV1.InferInput / InferOutput. Sync-only consumers may throw if validate returns a Promise. Implementers listed on the site: Zod 3.24+, Valibot 1.0+, ArkType 2.0+, Effect Schema 3.13+, yup 1.7+, joi 18+, typia 9.2+, Mongoose 9.7+, VineJS 4+, protovalidate-es, Arri, Sury, decoders, remult, Paseri, Lex SDK and about fifteen smaller ones. Consumers: tRPC, TanStack Form and Router, Hono, Elysia, oRPC, React Hook Form, next-safe-action, RTK Query, Inngest, Restate, FastMCP, xsMCP, Muppet. Guidance followed by PermDock: install @standard-schema/spec as a regular dependency because it is part of the public API. Sources: standardschema.dev, schema, repo. See Standard Schema.
TypeScript 7
typescript@7.0.2 published 2026-09-06 (ESM package). 7.0 is the native compiler with roughly 8 to 12 times faster builds and no stable programmatic compiler API (planned for 7.1), so ts-morph, api-extractor and typedoc-style codegen break until updated; 6.0 was the bridge that turned deprecations into errors (strict default true, moduleResolution: node10 removed, baseUrl deprecated). Library implications adopted in ADR 0016: isolatedDeclarations so .d.ts emit can be done by fast tools; erasableSyntaxOnly for Node type stripping, Bun and Deno; keep permission keys bounded and avoid deep recursive conditional types because type-level limits are unchanged; derive codegen from runtime definitions or Standard JSON Schema, never the compiler API. Exact 7.0 release date and the 7.1 API timeline are approximate.
OpenAPI 3.1 security
components.securitySchemes (apiKey, http with scheme: bearer, oauth2 with flows.*.scopes, openIdConnect, mutualTLS); root-level security as default; per-operation security overrides (empty array means public); alternatives are ORed across array entries and ANDed within an object; scopes are only meaningful for oauth2 and openIdConnect; x-* extensions are allowed on any object. How generators attach security:
@hono/zod-openapi1.6.3:app.openAPIRegistry.registerComponent('securitySchemes', ...)andcreateRoute({ security: [...] }). Repo.hono-openapi1.3.1:describeRoute({ security: [...] })per route pluscomponents.securitySchemesin generator options. Repo.@orpc/openapi1.15.0:oo.spec(middleware, spec => ({ ...spec, security: [...] }))extends operation objects from middleware, the cleanest hook for an authorization middleware. Docs.trpc-to-openapi3.3.0:.meta({ openapi: { protect: true } })maps to global security schemes; no per-scope granularity. Repo.@asteasolutions/zod-to-openapi9.1.0:registry.registerComponent('securitySchemes', ...),registry.registerPath({ security }). Repo.next-openapi-gen: no in-process hook; security comes from a JSDoc@authtag andauthPresets, or from Overlay files applied inoverlay.applybefore the spec is written. Scans Next.js, TanStack Start, React Router, Remix, SvelteKit, Nuxt, Astro, Hono and Express route files; scaffolds Scalar; compiles Arazzo. Repo.
Nothing in any authorization library populates these. PermDock targets OpenAPI 3.2 instead (ADR 0014), hooks into every generator that exposes an in-process hook, and hands an Overlay to those that do not (openapi adapter). The generators are one stage of a longer pipeline: appliers (next-openapi-gen, Redocly CLI), SDK generators (Hey API, Orval), docs UIs (Scalar) and OpenAPI-to-MCP bridges all read the applied description and need only standard security. That pipeline, and the rule that PermDock composes with it rather than wrapping any tool in it, are on OpenAPI ecosystem and in ADR 0023.
Gap matrix
Legend: yes, partial, no.
| Capability | CASL | permix | Kilpi | accesscontrol | Better Auth | Cerbos | OpenFGA | ZenStack | zap/permit |
|---|---|---|---|---|---|---|---|---|---|
| Inferred keys from definition | partial (declared tuple) | yes | yes | partial | yes (as const) | no | no | n/a (DSL) | yes |
| Standard Schema resources | no | no | no | no | no | no | no | no | yes |
| Conditions to where clause | yes (Prisma, Mongoose) | no | no | no | no | yes (Prisma, Drizzle) | partial (listObjects) | yes (compiled SQL) | no |
| Field-level | yes | no | partial (redact) | yes (attributes) | no | partial | no | yes | no |
| React + Next RSC + RN | partial (React only) | partial (React, Next; RN undocumented) | partial (RSC, client) | no | partial (via auth SDKs) | partial (React) | no | no | no |
| Vue / Svelte / Solid | Vue, Angular, Aurelia | yes, all | no | no | no | no | no | no | no |
| Hono / Elysia / Fastify / tRPC / oRPC | no | yes | partial (examples) | no | no | no | no | partial (adapters) | no |
| SSR hydration API | partial (pack/unpack) | yes | partial (client fetch) | no | no | no | no | no | no |
| Non-blocking or streaming page checks | no | partial (per-request cache) | partial (async RSC) | no | no | partial (hooks with loading) | no | no | no |
| Explain why denied | partial (reason, relevantRuleFor) | no | no | partial (events) | no | partial (audit, tests) | partial (expand) | no | partial (structured errors) |
| Audit hooks | partial | partial (hooks) | yes (plugin) | yes (events) | no | yes | yes | no | partial (OTel) |
| OpenAPI security emission | no | no | no | no | no | no | no | no | no |
| MCP tool binding | no | no | no | no | no | no | partial (guide) | no | no |
llms.txt or skills | no | yes, both | no | no | yes (llms) | yes (llms) | yes (llms) | no | yes (llms) |
| ESM-only | dual | yes | yes | yes | yes | yes | dual | yes | yes |
Gaps nobody fills well
- Standard-Schema-native definitions. Only
@zap-studio/permitconsumes~standard; nobody derives both resource types and a JSON Schema export of the permission model. PermDock:resource(Schema, ...)gives inferred permission types, typed conditions onInferOutput, and a JSON Schema catalog for docs and agents. - MCP tool authorization. The SDK gives
authInfo.scopesandscopeChallenge; SaaS options are proxies. PermDock:permissiononregisterToolfilterslist_tools, wraps handlers, and emits scope challenges from permission keys (mcp adapter). - React Native plus Next.js App Router in one library. permix is closest but RN is undocumented; CASL works in RN with do-it-yourself hydration; Clerk covers both only for its own permissions. PermDock: one snapshot format feeds
permdock/react,permdock/react-nativeandpermdock/next. - Non-blocking per-page checks. Nobody streams decisions: an RSC guard that resolves inside Suspense, batched client decision fetches, stale-while-revalidate, optimistic UI. PermDock:
usePermission()returns{ allowed, status }andProtectedrenders the shell immediately (Next.js 16.3 research). - OpenAPI emission. All generators expose a hook; no authorization library populates it.
- AI-agent-first docs. Best in class today: permix (
llms.txt,llms-full, skills), Payload (Claude plugin skills), the SaaS vendors (llms.txt). Missing everywhere: JSON Schema export of policies, a docs MCP server, explain or dry-run tooling agents can call. - Explain-why-denied and dry-run. CASL's
reasonplusrelevantRuleForis the high-water mark; PDPs rely on audit logs. A structured decision with matched grant, denials and alternatives, and asimulate()pre-flight, is uncontested (decisions). - Conditions to SQL without ORM lock-in. CASL (Prisma, Mongo), Cerbos (query plan), ZenStack (compiled), Oso Cloud (
listLocal) and typed-policy (Drizzle) each do one flavour; none offers Drizzle, Prisma, Kysely and raw SQL from a Standard-Schema-typed condition AST that also evaluates to a boolean on the client (conditions, rls adapter).
Verification notes
Could not verify: Cerbos, OpenFGA and Oso Cloud type generation from policies or models; the @zap-studio/permit adapter list beyond its README; permix and Kilpi behaviour in React Native; the ZenStack v3 server adapter list; Arcade TS SDK requires_auth parity; Keel repo status; exact TS 7.0 release date; AuthZEN support in Cedar, Amazon Verified Permissions, OPA, OpenFGA, Auth0 FGA, SpiceDB and WorkOS FGA (the AuthZEN interop site lists participants as images and the vendors' docs consulted do not mention it); vendor performance and SLA figures for Auth0 FGA and WorkOS FGA; the corporate lineage of WorkOS FGA (only third-party sources describe it, so it is left out); OPA and Cedar type generation from schemas; OPA star count (not fetched). Several packages named in the original brief do not exist on npm; role-acl and permissio are unmaintained since 2022; the original oso npm package is deprecated in favour of oso-cloud.
Adopt / adapt / avoid
Adopt:
- permix's single-package, subpath-export layout and its
llms.txtplus shipped-skills posture. - CASL's proof that one condition AST can drive both in-memory checks and ORM
whereclauses. @zap-studio/permit's Standard Schema resources and@standard-schema/specas a regular dependency.- Kilpi's discriminated decision object and layered assert handlers.
- Cerbos's embedded (WASM) PDP idea, as motivation for a client snapshot that answers offline.
- oRPC's middleware-attached OpenAPI spec extension as the model for emitting security from a guard.
- Cedar's discipline of validating policies against a schema at authoring time rather than at evaluation, and of returning the determining policies with every decision: PermDock's
permdock collect --checkandpermdock doctorcatch dangling references before deploy, andDecision.matched/denialsname the grants that decided. - Cedar's implicit-deny plus forbid-overrides-permit combining rule and accesscontrol's
tryCan()/require()gates as independent confirmation that fail-closed and deny-overrides-allow are the right defaults, not opinions.
Adapt:
- Better Auth
createAccessControlroles become a provider input, not a competitor. - OpenFGA and SpiceDB relation graphs are bridged through a provider rather than reimplemented.
- ZenStack's compiled-policy approach becomes
permdock.where()compilers plus RLS export, with parity tests as the glue, while keeping the DB-freecan()that ZenStack v3 dropped. - The Permit MCP Gateway's trust-level idea becomes per-tool
permissionbindings inside the server, not a proxy. - OpenFGA's
ListObjectsand SpiceDB'sLookupResourcesbecome thefilterimplementation of thepdpprovider presets, withwherecompiling toin(row.id, ids), exactly theWHERE id = ANY(...)pattern both vendors document; OPA's Compile API data filters (ucast+prisma,sql+postgresql) show that an external engine can emit a where clause, which is the barpermdock/drizzle,prismaandkyselymust at least meet in-process. - Amazon Verified Permissions' policy store (one per application or tenant, schema-validated,
BatchIsAuthorized) is the reference for what a hosted PDP offers;permdock/authzenshould expose the same three shapes (single, batch, token-authenticated principal) over AuthZEN rather than a custom API. - Casbin's PERM split of request, policy, matcher and effect is a useful checklist for the wire format: PermDock's
Decisionand catalog must make each of those four visible for audit even though the model itself is fixed.
Avoid:
- Strings-in, boolean-out SDKs as the primary API.
- Type-only definitions (permix) or declared tuples (CASL) as the typing strategy.
- Required runtime dependencies in core (Kilpi's
zodandsuperjson, permit's@opentelemetry/apipeer). - Vendor-locked permissions (Clerk) and DSLs that need their own compiler (ZModel, Polar, Cedar, Rego) as the authoring format.
- Relying on the TypeScript compiler API for codegen while TS 7.0 has none.
- Making deny-override or fail-closed configurable, as Casbin's effect expression and OPA's convention-based
allowrules do; in PermDock they are invariants. - Modelling relationship graphs (arrows, usersets, recursive membership) in the portable condition AST; that is the Zanzibar family's job and the
pdpbridge is the boundary.
Decisions informed
- ADR 0001: greenfield, MIT
- ADR 0002: one umbrella package
- ADR 0003: reference-based permissions
- ADR 0007: decide, not explain
- ADR 0010: policy as data, portable conditions
- ADR 0011: AuthZEN wire format
- ADR 0014: OpenAPI 3.2
- ADR 0015: no runtime dependencies in core
- ADR 0016: repo layout and toolchain
- ADR 0013: three-outcome decision: none of Cedar, AVP, OPA, the Zanzibar family, Casbin or accesscontrol has an approval outcome; each answers allow or deny, which is why
approval-requiredis a PermDock-level concept rather than something mapped from a PDP. - ADR 0012: two-principal subject: OpenFGA and SpiceDB model agents as separate principals with their own tuples; PermDock's
actorplusdelegationintersection is the in-process counterpart and is forwarded to remote PDPs insubject.properties. - Pages shaped: comparison, Standard Schema, openapi, mcp, better-auth, clerk, pdp, drizzle, prisma, kysely, rls, conditions, decisions, for AI agents.
Lessons from permix
What 25 rejected pull requests against permix and its issue tracker taught us about what a TypeScript permissions library must get right from day one.
CASL v7 deep-dive
Source study of @casl/ability 7.0.1, @casl/react 7.0.1 and @casl/prisma 2.0.2, and what PermDock adopts, adapts and avoids from the most mature in-process authorization library.