PermDock
Research

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/permit builds resource definitions on ~standard, despite 30+ validators implementing the spec and Zod and ArkType also implementing Standard JSON Schema.
  • Nobody emits OpenAPI security or scopes from permissions. Every generator (@hono/zod-openapi, hono-openapi, @orpc/openapi, trpc-to-openapi, zod-to-openapi, next-openapi-gen) requires hand-attaching security per 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 where clauses and does field-level permissions. v7 (May 2026) modernised it, but typing is declared, not inferred, and there is no llms.txt.
  • Explain-why-denied is universally weak. CASL has reason on rules and relevantRuleFor; 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.2 was 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 action plus subject plus optional MongoDB-style conditions (via @ucast) plus fields (glob patterns) plus inverted. Isomorphic.
  • Typing: AbilityBuilder<MongoAbility<[Actions, Subjects]>>; the action and subject tuple is declared, not inferred from a definition object. Subject types come from classes or subject('Post', obj); object detection needs detectSubjectType.
  • 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 / unpackRules for compact serialisation (SSR hydration is do-it-yourself), ability.update(rules) with on('updated'), ForbiddenError.from(ability).throwUnlessCan(...), reason on rules, relevantRuleFor().
  • Data layer: accessibleBy(ability, 'read').ofType('Post') returns a Prisma WhereInput; v2 no longer throws on no rules, you add createCaslExtension so empty conditions fail closed (Prisma 4 to 7; Prisma 7 needs the @casl/prisma/runtime wrapper). rulesToQuery was replaced by rulesToCondition in v7; rulesToAST for custom SQL.
  • None of Standard Schema, Zod, OpenAPI; casl.js.org/llms.txt returns 404. Dual CJS/ESM, about 6.17 kB min+gzip core.
  • v7 breaking changes (2026-05-21): PureAbility renamed Ability, rulesToQuery renamed rulesToCondition, 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 Next cache().
  • No validation, no OpenAPI, no explain. llms.txt and llms-full.txt served; 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) or Deny(), subject resolved by getSubject(); server-first.
  • Typing: subject narrowing after grant, resource parameter types, policy keys inferred from the policies object. No Standard Schema (Zod internally).
  • Adapters: RSC plugin (Authorize component), @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() and assert(), protected queries that co-locate authorisation with data fetching and redact fields, onUnauthorizedAssert, onAfterAuthorization, AuditPlugin with batch, periodical, manual and immediate strategies and waitUntil for serverless.
  • No data layer (redaction is post-fetch), no OpenAPI, llms.txt returns 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') returning granted and attributes; CRUD helpers are sugar over .action() / .do() so custom actions such as publish:own work; .extend() inheritance with deny-overrides; possession any versus own, enforced when policy.ownerField or 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 with and / 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 via setup(); defineCondition plus grantedAsync for async custom checks.
  • Runtime: glob attribute lists (['*', '!password', 'profile.*']) and filter() for field-level output; tryCan() never throws; access, change and error events; snapshot() / restore() and getGrantsList() for database persistence; lock(); prototype-pollution-safe name handling; strict mode for unknown roles and actions; nestjs-accesscontrol as 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 where clause or RLS); no client snapshot, no UI helpers, no OpenAPI, MCP or AuthZEN, no llms.txt.
  • Sources: repo, npm.

Smaller and unmaintained

  • @rbac/rbac 2.2.2 (published 2026-08-28): small RBAC with role inheritance and async can().
  • role-acl 4.5.4 and permissio 2.0.2 (LGPL-3.0): unmaintained since 2022.
  • @typed-policy/core 0.4.0 (MIT, 0 stars, February 2026): one policy AST (eq, neq, inArray, gt, lt, exists) with evaluate() on the client and compileToDrizzle() for a where fragment. Conceptually the "one definition, SQL and UI" idea; tiny and unproven.
  • @permx/core 0.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-prisma do 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 document with relations such as define viewer: [user], usersets like organization#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 request context, default evaluation cost cap of 100) and contextual tuples.
  • API: Check, BatchCheck (the docs suggest parallel Check under 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) and ListUsers. 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). ListObjects returns ids to feed WHERE id IN (...), not a where-clause compiler.
  • Agents: the use cases section covers AI agent authorization, RAG filtering and MCP server authorization: a tool type with define can_call: [user:*, user, role#assignee], a check on 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.txt serves. 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 .zed schema language: definition per object type, relation with allowed subject types (user, group#member, wildcards user:*), permission as 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: LookupResources ids into WHERE id = ANY(ARRAY[...]) when the accessible set is small (the docs suggest under 10,000), CheckBulkPermissions over 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.txt serves. 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 one workspace-admin assignment 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/node exposes workos.authorization.check({ organizationMembershipId, permissionSlug, resourceExternalId, resourceTypeSlug }) returning authorized. 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 .conf file. r = sub, obj, act defines the request tuple, p = sub, obj, act, eft the policy shape, m = r.sub == p.sub && r.obj == p.obj && r.act == p.act the 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] with g = _, _ (and g2 for resource roles); g, alice, data2_admin grouping policies; matcher g(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 into enforce and reference fields in the matcher (m = r.sub == r.obj.Owner), or store rules in the policy and evaluate them with eval(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. The in matcher 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 permit or forbid with a mandatory principal, action, resource scope (==, in, is) and optional when / unless conditions over attributes and context; annotations (@id, @advice) carry metadata without affecting evaluation. Combining rule: at least one permit and zero forbid means allow; any forbid or no permit means deny (implicit deny, explicit deny overrides).
  • Schema: entity types with attributes, optionality and parent (membership) relations; actions with appliesTo principal and resource types and a context shape; 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 the cedar-policy-symcc crate 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 in entities) and IsAuthorizedWithToken (principal from a Cognito or OIDC ID or access token; principal attributes come only from the token), responses ALLOW or DENY with determiningPolicies and errors; 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 to where clauses 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> returning result and, 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-Authenticate with scope hints; insufficient_scope enables 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/sdk 1.30.0; v2 @modelcontextprotocol/server 2.0.0 (published 2026-07-28, ESM, Node 20+). requireBearerAuth attaches AuthInfo (scopes, clientId, expiresAt) to ctx.http.authInfo; per-tool checks are hand-written in handlers returning isError: true. The scopeChallenge option on registerTool (PR #1624) is the one declarative hook and is what permdock/mcp builds 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_id per 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-openapi 1.6.3: app.openAPIRegistry.registerComponent('securitySchemes', ...) and createRoute({ security: [...] }). Repo.
  • hono-openapi 1.3.1: describeRoute({ security: [...] }) per route plus components.securitySchemes in generator options. Repo.
  • @orpc/openapi 1.15.0: oo.spec(middleware, spec => ({ ...spec, security: [...] })) extends operation objects from middleware, the cleanest hook for an authorization middleware. Docs.
  • trpc-to-openapi 3.3.0: .meta({ openapi: { protect: true } }) maps to global security schemes; no per-scope granularity. Repo.
  • @asteasolutions/zod-to-openapi 9.1.0: registry.registerComponent('securitySchemes', ...), registry.registerPath({ security }). Repo.
  • next-openapi-gen: no in-process hook; security comes from a JSDoc @auth tag and authPresets, or from Overlay files applied in overlay.apply before 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.

CapabilityCASLpermixKilpiaccesscontrolBetter AuthCerbosOpenFGAZenStackzap/permit
Inferred keys from definitionpartial (declared tuple)yesyespartialyes (as const)nonon/a (DSL)yes
Standard Schema resourcesnonononononononoyes
Conditions to where clauseyes (Prisma, Mongoose)nonononoyes (Prisma, Drizzle)partial (listObjects)yes (compiled SQL)no
Field-levelyesnopartial (redact)yes (attributes)nopartialnoyesno
React + Next RSC + RNpartial (React only)partial (React, Next; RN undocumented)partial (RSC, client)nopartial (via auth SDKs)partial (React)nonono
Vue / Svelte / SolidVue, Angular, Aureliayes, allnonononononono
Hono / Elysia / Fastify / tRPC / oRPCnoyespartial (examples)nonononopartial (adapters)no
SSR hydration APIpartial (pack/unpack)yespartial (client fetch)nononononono
Non-blocking or streaming page checksnopartial (per-request cache)partial (async RSC)nonopartial (hooks with loading)nonono
Explain why deniedpartial (reason, relevantRuleFor)nonopartial (events)nopartial (audit, tests)partial (expand)nopartial (structured errors)
Audit hookspartialpartial (hooks)yes (plugin)yes (events)noyesyesnopartial (OTel)
OpenAPI security emissionnonononononononono
MCP tool bindingnonononononopartial (guide)nono
llms.txt or skillsnoyes, bothnonoyes (llms)yes (llms)yes (llms)noyes (llms)
ESM-onlydualyesyesyesyesyesdualyesyes

Gaps nobody fills well

  1. Standard-Schema-native definitions. Only @zap-studio/permit consumes ~standard; nobody derives both resource types and a JSON Schema export of the permission model. PermDock: resource(Schema, ...) gives inferred permission types, typed conditions on InferOutput, and a JSON Schema catalog for docs and agents.
  2. MCP tool authorization. The SDK gives authInfo.scopes and scopeChallenge; SaaS options are proxies. PermDock: permission on registerTool filters list_tools, wraps handlers, and emits scope challenges from permission keys (mcp adapter).
  3. 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-native and permdock/next.
  4. 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 } and Protected renders the shell immediately (Next.js 16.3 research).
  5. OpenAPI emission. All generators expose a hook; no authorization library populates it.
  6. 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.
  7. Explain-why-denied and dry-run. CASL's reason plus relevantRuleFor is the high-water mark; PDPs rely on audit logs. A structured decision with matched grant, denials and alternatives, and a simulate() pre-flight, is uncontested (decisions).
  8. 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.txt plus shipped-skills posture.
  • CASL's proof that one condition AST can drive both in-memory checks and ORM where clauses.
  • @zap-studio/permit's Standard Schema resources and @standard-schema/spec as 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 --check and permdock doctor catch dangling references before deploy, and Decision.matched / denials name 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 createAccessControl roles 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-free can() that ZenStack v3 dropped.
  • The Permit MCP Gateway's trust-level idea becomes per-tool permission bindings inside the server, not a proxy.
  • OpenFGA's ListObjects and SpiceDB's LookupResources become the filter implementation of the pdp provider presets, with where compiling to in(row.id, ids), exactly the WHERE 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 bar permdock/drizzle, prisma and kysely must 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/authzen should 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 Decision and 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 zod and superjson, permit's @opentelemetry/api peer).
  • 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 allow rules 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 pdp bridge is the boundary.

Decisions informed

On this page