# Comparison

Source: https://permdock.com/docs/comparison

How PermDock differs from permix, CASL, Kilpi, zap-studio/permit, Better Auth access control, Cedar and Amazon Verified Permissions, Open Policy Agent, the Zanzibar family (OpenFGA, Auth0 FGA, SpiceDB, WorkOS FGA), Casbin, accesscontrol, hosted PDPs, ZenStack and the AI SDK OPA adapter.

Every PermDock capability below exists in the package unless it is marked planned here or its own page carries `Status: planned`. The other libraries are described as they were in September 2026, based on the [landscape survey](/docs/research/landscape). Where a fact could not be verified it is left out rather than guessed.

The short version: the TypeScript space splits into in-process libraries with good key inference but weak data-layer and agent stories (permix, CASL, Kilpi, permit, Better Auth, accesscontrol, Casbin) and external policy engines with strings in and booleans out (Cedar and Amazon Verified Permissions, Open Policy Agent, the Zanzibar family of OpenFGA, Auth0 FGA, SpiceDB and WorkOS FGA, plus Cerbos, Permit.io and Oso Cloud). PermDock tries to be an in-process library with typed references over Standard Schema resources, one condition format that reaches SQL and RLS, structured decisions, and first-class MCP, AI SDK and AuthZEN surfaces.

## permix [#permix]

permix v4.1.2 is the closest library in adapter breadth: React, Vue, Solid, Svelte, Next.js App Router (per-request instance via React `cache()`), TanStack Start, Node, Express, Hono, Fastify, Elysia, tRPC, oRPC, Effect and Drizzle, all as subpath exports of one 2.64 kB gzip package, with `llms.txt` and agent skills. Its typing is template-literal keys (`check('post.create')`), its core is a mutable global `setup()`, hydration collapses function rules to booleans, there is no condition AST (so no SQL or RLS), no explain, no Standard Schema, no OpenAPI, no MCP, and React Native is undocumented ([landscape](/docs/research/landscape)).

## CASL v7 [#casl-v7]

CASL (`@casl/ability` 7.0.1, about 1.4M weekly downloads) is the only mature in-process library that compiles conditions to Prisma and Mongoose `where` clauses and does field-level permissions. v7 (May 2026) modernised it: `rulesToCondition`, frozen rule arrays, `reason` on rules, `relevantRuleFor`. Typing is declared `[Actions, Subjects]` tuples rather than inferred from a definition; subject detection needs classes or `subject()` wrappers; there is no Standard Schema, no SSR hydration API beyond `packRules`, no Next.js, React Native, MCP or OpenAPI story, a dual CJS/ESM build, and no `llms.txt`. PermDock adopts CASL's rule-as-data and one-AST-many-interpreters architecture and drops subject detection and the shared `can` name ([landscape](/docs/research/landscape)).

## Kilpi v1 [#kilpi-v1]

Kilpi 1.1 is server-first: policies are functions returning `Grant(subject)` or `Deny()`, `authorize()` is async, protected queries redact fields post-fetch, an audit plugin batches decision events, an RSC plugin offers `Access` components, and `@kilpi/client` fetches decisions from an endpoint with caching and batching. It has 89 stars and no commits since December 2025. It has no Standard Schema (Zod internally), no data-layer `where`, no React Native, MCP or OpenAPI, `zod` and `superjson` in core, and `$`-prefixed instance members. PermDock keeps Kilpi's discriminated decision, subject narrowing through `assert`, layered unauthorized handlers and batched client decisions, and drops the rest ([landscape](/docs/research/landscape)).

## @zap-studio/permit [#zap-studiopermit]

`@zap-studio/permit` 2.0.1 is the only authorization library built on Standard Schema: resources are validators, rule parameter types are inferred, `allow` / `deny` / `when` compose policies, evaluation is fail-closed, and it is about 2.8 kB minified with structured errors and OpenTelemetry hooks. It ships no adapters, no hydration, no OpenAPI and no MCP, validates on every call, requires a resource object for every action, and makes `@opentelemetry/api` a required peer. PermDock adopts the Standard Schema resources, fail-closed evaluation and span-per-check, and makes validation a boundary concern ([landscape](/docs/research/landscape)).

## Better Auth access control [#better-auth-access-control]

Better Auth's `createAccessControl(statement)` plus `newRole()` gives typed RBAC from an `as const` statement, with async `hasPermission` on server and client, a synchronous `checkRolePermission` for static roles, and dynamic roles stored in the database. It is RBAC only (no conditions), has no UI helpers, hydration or data layer, and is coupled to Better Auth. PermDock layers on top through the `better-auth` provider: Better Auth resolves the session and roles, PermDock owns permissions, conditions and decisions ([Better Auth provider](/docs/adapters/better-auth)).

## Hosted and policy-language PDPs [#hosted-and-policy-language-pdps]

Cerbos (YAML policies with CEL, embedded WASM PDP, query plan to Prisma and Drizzle, React hooks, AuthZEN support), Permit.io (OPA/OPAL-backed PDP, an MCP Gateway that proxies agent traffic), OpenFGA (Zanzibar ReBAC, `listObjects` for filtering, an MCP authorization guide), SpiceDB (Zanzibar with CEL caveats, gRPC SDK) and Oso Cloud (Polar rules, `listLocal` SQL fragments, "Oso for Agents" monitoring) all evaluate policies outside your TypeScript code. Their SDKs take strings and return booleans; none infer types from your model; MCP support is a proxy or a guide, not something you embed in your server. PermDock is embedded and typed, and interoperates with all of them through AuthZEN: its decision endpoint is an AuthZEN PDP and its `pdp` provider is an AuthZEN client. It also offers what these vendors sell, without moving the decision: PermDock Cloud is an optional hosted AuthZEN Authorization Decision Service plus approval inbox and decision log that implements the same interfaces the open-source package ships with in-process defaults, so a Go service or an API gateway can enforce a TypeScript-authored policy while the TypeScript app keeps deciding locally. Zanzibar-scale relation graphs remain a non-goal; bridge to OpenFGA or SpiceDB through a provider. The sections below on Cedar, OPA and the Zanzibar family go into the engines that were only named here.

## ZenStack v3 [#zenstack-v3]

ZenStack v3 (3.9.3) puts `@@allow` / `@@deny` policies in a Prisma-compatible ZModel schema and compiles them to SQL through Kysely, so reads are filtered and writes rejected at the query layer, with an auto-generated CRUD API and TanStack Query hooks. v3 removed the DB-free `check()` that v2 offered, so there is no way to ask "can this user?" in the UI without a database round-trip, policies are a DSL rather than TypeScript, and there is no `llms.txt`. PermDock keeps checks in-process and portable to both UI and SQL, and does not own the ORM.

## @ai-sdk/policy-opa [#ai-sdkpolicy-opa]

Vercel's reference policy adapter for AI SDK 7 tool approvals evaluates Rego (via WASM or an HTTP OPA) inside `toolApproval`, narrows the tool list with `opaCapabilityMiddleware`, and offers a `shadow` mode. It fails open on unrecognised decisions ([vercel/ai#19978](https://github.com/vercel/ai/issues/19978)) and requires writing Rego with no link to the application's permission model. `permdock/ai-sdk` fills the same hooks from the app's typed policy, maps the three-outcome `Decision` to `approved` / `denied` / `user-approval`, never returns `not-applicable`, and re-checks approvals against a replay-safe token ([AI SDK adapter](/docs/adapters/ai-sdk)).

## Cedar and Amazon Verified Permissions [#cedar-and-amazon-verified-permissions]

[Cedar](https://docs.cedarpolicy.com/) is an Apache-2.0 policy language from AWS implemented in Rust, with `@cedar-policy/cedar-wasm` (4.12.0 on npm, published 2026-07-28) providing WASM bindings and TypeScript types for the engine's API. A policy is `permit` or `forbid` over a `principal`, `action`, `resource` scope with `when` / `unless` conditions; the default is deny and any matching `forbid` overrides every `permit`, the same combining rule as PermDock's deny-overrides-allow invariant ([policy syntax](https://docs.cedarpolicy.com/policies/syntax-policy.html)). A [schema](https://docs.cedarpolicy.com/schema/schema.html) declares entity types, their attributes and parent relations, and which actions apply to which principal and resource types with what `context` shape; the validator type-checks policies against it at authoring time rather than at evaluation, and the `cedar-policy-symcc` crate verifies properties about policy sets with concrete counterexamples ([repo](https://github.com/cedar-policy/cedar)). Principals, resources and their attributes are passed to the authorizer as an entity slice with each request.

[Amazon Verified Permissions](https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/what-is-avp.html) hosts Cedar policies in [policy stores](https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/policy-stores.html) (one per application or tenant, with an optional schema that rejects invalid policies), evaluates them through [`IsAuthorized`](https://docs.aws.amazon.com/verifiedpermissions/latest/apireference/API_IsAuthorized.html), [`BatchIsAuthorized`](https://docs.aws.amazon.com/verifiedpermissions/latest/apireference/API_BatchIsAuthorized.html) (up to 30 requests that share a principal or a resource) and [`IsAuthorizedWithToken`](https://docs.aws.amazon.com/verifiedpermissions/latest/apireference/API_IsAuthorizedWithToken.html) (principal taken from a Cognito or OIDC JWT), returns `ALLOW` or `DENY` with the `determiningPolicies` that produced the decision, and offers an Express middleware integration. AVP states that it currently uses Cedar version 4.7.

How PermDock relates: Cedar's `determiningPolicies` is the closest published analogue to PermDock's `matched` and `denials`, and its schema-validated policies are the model for `permdock collect --check` and `permdock doctor` catching references to permissions that do not exist. The differences are the authoring format and the type story. Cedar policies are a separate language with their own schema; PermDock policies are TypeScript data whose types come from the Standard Schema you already have, so `permissions.post.update` is a reference the compiler checks, whereas `Action::"updatePhoto"` is a string the validator checks later. Cedar evaluates over an entity slice you assemble; PermDock's portable conditions also compile to `where` clauses and RLS so the database does the filtering. Neither Cedar nor AVP has an approval outcome. AVP is a hosted PDP with its own request shape rather than AuthZEN, so pairing it with [`permdock/pdp`](/docs/adapters/pdp) needs an AuthZEN-speaking front; PermDock ships no AVP-specific mapping.

## Open Policy Agent and Rego [#open-policy-agent-and-rego]

[Open Policy Agent](https://www.openpolicyagent.org/docs/latest/) is a CNCF-graduated, general-purpose policy engine: policies are written in Rego over arbitrary JSON input and data, and OPA is queried by microservices, Kubernetes admission control, CI pipelines, API gateways and Envoy external authorization alike. It runs as a daemon or sidecar with a [REST API](https://www.openpolicyagent.org/docs/latest/rest-api/) (`POST /v1/data/<path>` with an `input` document returns `result` and a `decision_id` for decision logs), as a Go library, or compiled to WebAssembly (`opa build -t wasm`) and loaded with [`@open-policy-agent/opa-wasm`](https://www.npmjs.com/package/@open-policy-agent/opa-wasm) (`loadPolicy`, `setData`, `evaluate`; 1.10.0, last published November 2024). Its Compile API performs partial evaluation and, with `targetDialects` such as `ucast+prisma`, `sql+postgresql` and `sql+mysql`, turns a Rego policy plus known input into data filters for your own database, which is the nearest thing among external engines to PermDock's `where`.

The trade is generality against integration. Rego is domain-agnostic, so nothing ties a rule to the application's resource types; the SDK is JSON in and JSON out with no inference from your model; deny-overrides-allow is a convention you write, not a property of the engine; and the deployment story assumes an OPA process or a WASM bundle you build separately. The docs already discuss OPA in agent form: `@ai-sdk/policy-opa` (above) evaluates Rego inside AI SDK tool approvals and fails open on unrecognised decisions. PermDock treats OPA as a peer PDP: with an AuthZEN front it becomes a `remotePdp` target ([pdp](/docs/adapters/pdp)); infrastructure policy (admission, gateways, CI) stays OPA's domain and is outside PermDock's scope.

## The Zanzibar family: OpenFGA, Auth0 FGA, SpiceDB, WorkOS FGA [#the-zanzibar-family-openfga-auth0-fga-spicedb-workos-fga]

These systems store relationship tuples (`user:anne` is `viewer` of `document:roadmap`) and compute permissions by walking a graph defined in a schema, following Google's Zanzibar paper. They answer "does this user have this relation to this object?" and its inverses at a scale an in-process library cannot, and PermDock does not try to ([roadmap](/docs/roadmap)).

* [OpenFGA](https://openfga.dev/docs/fga) is the CNCF-owned open source engine. The model DSL declares types and relations (`type document` with `define viewer: [user]`), tuples are written through the API, and the [query endpoints](https://openfga.dev/docs/interacting/relationship-queries) are `Check`, `BatchCheck`, `Read`, `Expand`, `ListObjects` (all objects of a type a user has a relation with, for access-aware filtering of small collections) and `ListUsers`. [Conditions](https://openfga.dev/docs/modeling/conditions) attach Google CEL expressions with typed parameters to tuples, evaluated with request `context` and capped at an evaluation cost of 100 by default; contextual tuples cover the rest of ABAC. SDKs exist for JavaScript, Go, Java, .NET and Python; Postgres, MySQL and SQLite are the production datastores. The [MCP server authorization](https://openfga.dev/docs/use-cases/mcp-server-authorization) pattern models a `tool` type with a `can_call` relation, checks it on every request and filters the tool list with list-objects.
* [Auth0 FGA](https://auth0.com/fine-grained-authorization) is the managed service built on OpenFGA, with the same model, tuples and check API, documented at [docs.fga.dev](https://docs.fga.dev/); Auth0 Lab records the product graduating as [Okta FGA](https://lab.auth0.com/experiments/sandcastle), so both names refer to the same service.
* [SpiceDB](https://authzed.com/docs/spicedb/concepts/schema) (AuthZed) uses a `.zed` schema of `definition`, `relation` and `permission` with union, intersection, exclusion and arrow (`parent_folder->read`) operators, CEL caveats on relationships, and gRPC APIs `CheckPermission`, `CheckBulkPermissions`, `LookupResources`, `LookupSubjects`, `WriteRelationships` and a Watch API with ZedTokens for consistency. Its [list-endpoint guide](https://authzed.com/docs/spicedb/modeling/protecting-a-list-endpoint) documents the three filtering strategies: `LookupResources` ids into `WHERE id = ANY(...)` when the accessible set is small, `CheckBulkPermissions` over a page of candidates otherwise, and Materialize (early access) for a denormalised local copy. AuthZed ships an MCP server and LangChain integrations.
* [WorkOS FGA](https://workos.com/docs/fga) is often grouped with this family but its current docs describe something narrower and deliberately DSL-free: resource types configured in the Dashboard, resources registered with a parent, roles and permissions scoped to a resource type that may include child-type permissions, and assignments that propagate down the hierarchy. Subjects are organization memberships and groups. The [access check](https://workos.com/docs/fga/access-checks) endpoints are `check`, `listEffectivePermissions`, `listResourcesForMembership` and `listMembershipsForResource`, called from `@workos-inc/node` as `workos.authorization.check(...)`; organization-scoped roles ride in the AuthKit JWT so org-wide checks need no API call.

How PermDock relates: the `openfga` and `spicedb` presets in `permdock/pdp` wrap `check` / `listObjects` and `CheckPermission` / `LookupResources` behind the provider interface, so `filter` returns the permitted ids and `where` compiles to `in(row.id, ids)`, which is exactly the vendors' own documented list-endpoint pattern. PermDock stays the typed PEP in front (references, boundary validation, `Decision`, Problem Details, MCP refusals, approvals) and never stores or models tuples. Note that the Zanzibar answer to agents is to model the agent as another principal with its own tuples; PermDock's two-principal subject and `approval-required` outcome are additive to that, not a replacement.

## Casbin and node-casbin [#casbin-and-node-casbin]

[Casbin](https://casbin.org/docs/overview) is an Apache-licensed authorization library with implementations in Go, Java, Node.js, PHP, Python, .NET, Rust and more. Access control is expressed as a `.conf` model file in the PERM metamodel (Policy, Effect, Request, Matchers) plus a policy file or database adapter: `r = sub, obj, act` defines the request, `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, so `some(where (p.eft == allow))` is allow-if-any while `some(where (p.eft == allow)) && !some(where (p.eft == deny))` gives deny-override ([how it works](https://casbin.org/docs/how-it-works)). Eighteen [model families](https://casbin.org/docs/supported-models) ship as examples, including RBAC with domains and tenants, ABAC through attributes such as `r.obj.Owner`, RESTful path matching, priority and deny-override. [node-casbin](https://github.com/apache/casbin-node-casbin) (`casbin` 5.51.1 on npm) exposes `newEnforcer(model, policy)`, `enforce(sub, obj, act)` and `enforceSync`, a Management API and an RBAC API (`getRolesForUser`), adapters for persistence and watchers for multi-node consistency; the `in` matcher operator is not yet available in Node-Casbin.

Casbin and PermDock differ on almost every axis that matters here. Casbin's request, policy and matcher fields are runtime strings with no TypeScript inference; the matcher is an expression string evaluated at runtime; deny-override is something the model author opts into with the effect expression, whereas PermDock makes it an invariant; there is no data-layer compilation, no UI or SSR story, and no agent surface. Casbin's strengths are polyglot parity, a model that can express Bell-LaPadula or priority policies PermDock does not attempt, and a runtime policy-management API. If several services in different languages need to enforce one policy, Casbin (or a PDP) fits and PermDock does not.

## accesscontrol [#accesscontrol]

[`accesscontrol`](https://github.com/onury/accesscontrol) (3.1.0, MIT, ESM) is a chainable role-and-attribute library: `ac.grant('user').createOwn('video')`, `ac.can('admin').updateAny('video').granted`, glob-notation attribute lists (`['*', '!password', 'profile.*']`) with `filter()` for field-level output, `.extend()` inheritance with deny-overrides, and possession (`any` versus `own`) enforced when a `policy.ownerField` or resolver is configured. v3 added a policy engine: `.where('$.order.value <= 100000')` conditions with a canonical JSON form, `require()` gates that can only restrict, custom actions through `.action()` / `.do()`, groups and categories, `defineCondition` for async custom checks, an `access` audit event stream, `tryCan()` that never throws, `snapshot()` / `restore()` for persistence, prototype-pollution-safe name handling and an official NestJS integration.

Its priorities overlap with PermDock's invariants (fail closed, deny wins, prototype safety, audit events) and it is the most complete pure in-process RBAC and ABAC engine in the survey. The gaps are the ones PermDock exists for: roles, resources and actions are strings, there is no Standard Schema or inferred instance type, conditions evaluate in memory only and never reach SQL or RLS, there is no snapshot for the client, no framework adapters beyond an Express example and the Nest package, and no MCP, AI SDK or AuthZEN surface.

## Feature matrix [#feature-matrix]

Legend: yes, partial (with a note), no, planned (not implemented yet). Hosted PDPs are summarised as a group; see the paragraphs above for which vendor does what.

| Capability | PermDock | permix | CASL v7 | Kilpi v1 | zap/permit | Better Auth AC | Hosted PDPs | ZenStack v3 | ai-sdk/policy-opa |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Typed references (not strings) | yes | no (template-literal keys) | no (declared tuples) | partial (inferred keys, Proxy paths) | no (string actions) | no (`as const` keys) | no | n/a (DSL) | no (Rego) |
| Standard Schema resources | yes | no | no | no | yes | no | no | no | no |
| Portable conditions to SQL / RLS | yes (Drizzle, Prisma, Kysely, RLS) | no | partial (Prisma, Mongoose `where`) | no | no | no | partial (Cerbos query plan, Oso `listLocal`, OpenFGA `listObjects`) | yes (compiled SQL) | no |
| Structured decisions / explain | yes (`Decision` with reasons, alternatives) | no | partial (`reason`, `relevantRuleFor`) | partial (`Deny` message, audit plugin) | partial (structured errors) | no | partial (audit logs, policy tests) | no | no |
| Snapshots carrying conditions | yes | no (booleans only) | partial (`packRules`) | no (fetch per decision) | no | no | no | no | no |
| React Server Components / Next.js | yes (Cache Components, `use cache: private`) | yes (per-request `cache()`) | no | partial (RSC `Access`) | no | partial (via Better Auth Next SDK) | no | no | no |
| React Native | yes (persisted snapshot) | undocumented | works, DIY | no | no | partial (Expo via Better Auth) | partial (fetch clients) | no | no |
| MCP tool authorization | yes (`scopeChallenge`, filtered `list_tools`) | no | no | no | no | no | partial (Permit gateway proxy, OpenFGA guide) | no | no |
| AI SDK approvals | yes (`toolApproval`, capability middleware, `needsApproval`) | no | no | no | no | no | no | no | yes (fails open on unknown) |
| OpenAPI `security` emission | yes (3.2 with 3.1 fallbacks; 3.3 draft behind `--target 3.3`) | no | no | no | no | no | no | no | no |
| AuthZEN | yes (PDP and PEP; in-repo Basic / Batch / Search / Discovery conformance, external certification planned) | no | no | no | no | no | partial (Cerbos) | no | no |
| RLS import into app permissions | yes (`permdock rls import`) | no | no | no | no | no | no | no | no |
| `llms.txt` / skills | yes (both, plus docs MCP at `/mcp`) | yes (both) | no | no | `llms.txt` | `llms.txt` | `llms.txt` (Cerbos, OpenFGA, SpiceDB, Oso) | no | n/a |
| Core size | budget about 3 kB gzip | 2.64 kB gzip | about 6.17 kB min+gzip | not measured | about 2.8 kB min | part of Better Auth | SDKs 2.6–5.6 MB unpacked (Permit, SpiceDB) | ORM-sized | WASM or HTTP |

### By engine [#by-engine]

The second table lists every product named on this page, including the policy engines the first table groups as "Hosted PDPs", against the seven axes that decide most adoption conversations. "Typed references" means the compiler checks a permission reference against your definition; "Agent approvals" means a distinct third outcome rather than allow or deny; "not verified" means the vendor's docs consulted for this page neither confirm nor deny.

| Product | Typed references | Standard Schema | Conditions compile to SQL / RLS | Agent approvals (`approval-required`) | AuthZEN | Embedded vs hosted | Policy language |
| --- | --- | --- | --- | --- | --- | --- | --- |
| PermDock | yes | yes | yes (Drizzle, Prisma, Kysely, RLS generate and import) | yes (with replay-safe token and a pluggable `ApprovalStore`) | yes (PDP via `permdock/authzen`, PEP via `permdock/pdp`, hosted ADS via PermDock Cloud) | embedded; hosted AuthZEN ADS optional, never required to decide | TypeScript data (`allow` / `deny` grants with portable conditions) |
| permix | no (template-literal keys) | no | no | no | no | embedded | TypeScript booleans and closures |
| CASL v7 | no (declared tuples) | no | partial (Prisma, Mongoose `where`) | no | no | embedded | JSON rules with MongoDB-style conditions |
| Kilpi v1 | partial (inferred keys) | no | no | no | no | embedded, server-first | TypeScript functions returning `Grant` / `Deny` |
| zap/permit | no (string actions) | yes | no | no | no | embedded | TypeScript (`allow`, `deny`, `when`) |
| Better Auth AC | no (`as const` keys) | no | no | no | no | embedded, coupled to Better Auth | `as const` statement plus roles |
| ZenStack v3 | n/a (DSL) | no | yes (compiled SQL via Kysely) | no | no | embedded in the ORM | ZModel `@@allow` / `@@deny` |
| @ai-sdk/policy-opa | no | no | no | yes (fails open on unrecognised decisions) | no | embedded WASM or HTTP OPA | Rego |
| Cedar (`cedar-wasm`) | no (`User::"alice"` entity literals) | no | not documented (evaluates over a supplied entity slice) | no | not verified | embedded (Rust crate or WASM) | Cedar, schema-validated |
| Amazon Verified Permissions | no | no | no | no | not verified | hosted (AWS) | Cedar, policy stores with templates |
| Open Policy Agent | no | no | partial (Compile API data filters to SQL and UCAST / Prisma; no RLS) | no | not verified | embedded (Go library, WASM) or daemon / sidecar | Rego |
| OpenFGA | no | no | partial (`ListObjects` ids) | no (agents modelled as principals) | not verified | self-hosted server | FGA model DSL plus relationship tuples, CEL conditions |
| Auth0 FGA (Okta FGA) | no | no | partial (`ListObjects` ids) | no | not verified | hosted (built on OpenFGA) | FGA model DSL plus tuples |
| SpiceDB / AuthZed | no | no | partial (`LookupResources` ids, `CheckBulkPermissions`, Materialize) | no | not verified | self-hosted or AuthZed Cloud | `.zed` schema plus relationships, CEL caveats |
| WorkOS FGA | no | no | partial (`listResourcesForMembership` ids) | no | not verified | hosted | none; resource types, roles and permissions configured in the Dashboard |
| Casbin / node-casbin | no | no | no | no | no | embedded | PERM `.conf` model plus CSV or adapter policy |
| accesscontrol | no | no | no | no | no | embedded | chainable API with JSON grants and `.where()` expressions |
| Cerbos | no | no | partial (query plan to Prisma, Drizzle) | no | yes | embedded WASM, sidecar or Hub | YAML with CEL |
| Permit.io | no | no | no | no | not verified | hosted PDP (OPA / OPAL) | UI-managed, OPA underneath |
| Oso Cloud | no | no | partial (`listLocal` SQL fragments) | no | not verified | hosted | Polar |

### Vocabulary map [#vocabulary-map]

The same idea has a different name in each engine. The table maps PermDock's terms to the four engines most often evaluated alongside it; a blank cell means the engine has no direct equivalent in the docs consulted.

| PermDock | Cedar / AVP | OPA / Rego | OpenFGA / SpiceDB | Casbin |
| --- | --- | --- | --- | --- |
| `permissions.post.update` (typed reference) | `Action::"updatePhoto"` entity literal, declared in the schema | a rule name such as `allow`, chosen by convention | a relation or permission name (`can_view`, `edit`) on an object type | `act` field of the request |
| `resource(Schema, ...)` | entity type with attributes and parents in the schema | `input` shape, unconstrained | `type document` with `relations` / `definition document` | `obj` field, a string |
| `role('member', [...])` | `principal in Group::"..."` or a `principal is User` scope | a rule over `input.user.roles` | `role#assignee`, `group#member` usersets | `g = _, _` role definition with `g, alice, data2_admin` grouping policies |
| `allow(...)` / `deny(...)` | `permit` / `forbid` (forbid always wins) | separate rules; deny-override written by hand | computed sets with union, intersection, exclusion (`-` in SpiceDB) | `p.eft` allow / deny with effect expression `some(where (p.eft == allow)) && !some(where (p.eft == deny))` |
| `where: { authorId: principal.id }` (portable condition) | `when { resource.owner == principal }` | Rego expression over `input` and `data` | CEL condition on a tuple (OpenFGA) or caveat (SpiceDB) | matcher expression such as `m = r.sub == r.obj.Owner`, or `eval(p.sub_rule)` over a rule stored in the policy |
| `decide()` with `matched` / `denials` | `IsAuthorized` decision plus `determiningPolicies` | `result` plus `decision_id` | `Check` boolean; `Expand` for the userset tree | `enforce` boolean |
| `simulate([...])` | `BatchIsAuthorized` (up to 30, shared principal or resource) | one query per decision | `BatchCheck` / `CheckBulkPermissions` | Batch API |
| `filter` / `where` |  | Compile API data filters (SQL, UCAST / Prisma) | `ListObjects` / `LookupResources` ids |  |
| `approval-required` |  |  |  |  |
| snapshot for the client |  | WASM bundle evaluated in the browser | Materialize (SpiceDB, early access) for services |  |

## Pick by use case [#pick-by-use-case]

**Single Next.js app with RBAC.** If roles are static, there are no per-row conditions and Better Auth or Clerk already issues the session, their built-in access control is enough and adding a library is overhead. The moment a rule reads the row (`authorId`, `orgId`, `published`), or the same check has to run in a Server Component, a client component during an instant navigation and a route handler, PermDock's typed references, snapshot and [`permdock/next`](/docs/adapters/next) are the fit; permix covers the same frameworks with booleans only. Keep Better Auth or Clerk as the [provider](/docs/adapters/better-auth) rather than replacing them.

**Supabase or RLS-heavy app.** Choose between owning the policy in the database or in the application. Hand-written RLS or ZenStack's compiled ZModel policies keep the database authoritative but leave the UI guessing. PermDock's answer is one portable condition that `permdock rls generate` turns into `CREATE POLICY` statements for Supabase, Neon or a GUC dialect, `import` reads back from `pg_policies` into `definePermissions()`, and `verify` proves parity against fixtures ([RLS adapter](/docs/adapters/rls), [Supabase provider](/docs/adapters/supabase)). If your policies already use `CASE`, multi-join subqueries or custom functions, expect them to import as `opaque` and stay database-only.

**Relationship graphs at scale.** Google-Drive-style sharing over millions of objects, arbitrary userset rewrites, intersection and exclusion over relations: use OpenFGA, Auth0 FGA or SpiceDB, and put the model and tuples there. PermDock covers the targeted cases over your own tables: parent chains, edge tables with a role column, implied relations (`includes`), groups nested up to 16 deep, and to-one links as arrows. Each compiles to Drizzle, Kysely and Prisma filters and to RLS ([relationships](/docs/concepts/relationships)). It keeps no tuple store and has no general rewrite language. PermDock can still be the PEP: `permdock/pdp` with an `openfga` or `spicedb` preset makes `decide` call `check`, `filter` call `listObjects` / `LookupResources`, and `where` compile to `in(row.id, ids)`, while references, boundary validation, Problem Details and approvals stay typed in the application ([pdp](/docs/adapters/pdp)). WorkOS FGA's hierarchy model fits the same slot once its API is wrapped, but there is no preset planned.

**Enterprise central PDP.** When a security team owns policy on its own release cycle, in Cedar (AVP), Rego (OPA) or a certified AuthZEN PDP such as Cerbos, Topaz or Keycloak, keep policy there. Any AuthZEN speaker is a `remotePdp` target for [`permdock/pdp`](/docs/adapters/pdp): PermDock builds the `subject`, `action`, `resource` request from typed references, folds the boolean answer into a `Decision`, fails closed on anything unrecognised, and still applies local `deny` grants and delegation intersection. AVP and OPA need an AuthZEN front first. In the other direction, [`permdock/authzen`](/docs/adapters/authzen) exposes a PermDock policy as an AuthZEN PDP so gateways and other services can ask it.

**Agent tool authorization.** MCP servers, AI SDK 7 tools and Claude Agent SDK hooks all need the same three answers: may this actor call this tool for this user, which tools should it even see, and which calls need a human first. The Zanzibar pattern (a `tool` type with `can_call`, list-objects for the visible set) and `@ai-sdk/policy-opa` (Rego in `toolApproval`) each answer the first two with a boolean. PermDock's [MCP](/docs/adapters/mcp), [AI SDK](/docs/adapters/ai-sdk) and [Claude Agent](/docs/adapters/claude-agent) adapters answer all three from the same typed policy: `approval: 'human'` grants produce `approval-required` with a token bound to permission key, resource id, subject and actor, the actor never exceeds the delegating user, and unknown decisions are denials, never executions ([approvals](/docs/security/approvals), [delegation](/docs/security/delegation)).

**Browser agents (WebMCP).** A page exposing tools through `document.modelContext.registerTool()` has to decide which tools this user may trigger before an in-page agent sees them. None of the engines above has a client-side story; a hosted PDP would need a network round-trip per registration. [`permdock/webmcp`](/docs/adapters/webmcp) registers one tool per action the current snapshot allows and unregisters when the snapshot changes, and the server-side route the tool calls re-checks with the same policy.

## When not to use PermDock [#when-not-to-use-permdock]

* You need a library with years of production mileage. PermDock is new; CASL, permix, accesscontrol, Casbin and every engine on this page have been installable for far longer.
* Policy is authored and deployed by people outside the application's release cycle. Cedar with AVP, OPA and Cerbos give you a policy store, a separate deploy pipeline and a language a security team can own; PermDock's policies are TypeScript that ships with the app.
* Several backends in different languages must enforce one policy. PermDock is TypeScript only. OPA, Cedar, Casbin and the Zanzibar servers have Go, Java, Python and .NET clients.
* Authorization is a large relationship graph with its own schema language. Unbounded nesting, rewrites across many object types and inheritance over large object counts belong in OpenFGA, Auth0 FGA or SpiceDB; PermDock's `pdp` bridge covers only the enforcement half.
* You need formal analysis of the policy set. Cedar's validator and symbolic compiler can prove properties and produce counterexamples; PermDock offers `simulate` and matrix tests, not proofs.
* The rules are pure RBAC with no conditions and you already run Better Auth, Clerk or WorkOS. Their built-in checks are sufficient and add no dependency.
* The database should be the single point of enforcement and the UI can afford a round-trip. ZenStack's compiled policies or hand-written RLS achieve that without an application-side model.
* The policy is about infrastructure rather than application resources: Kubernetes admission, gateway routing, CI gates. That is OPA's domain.

## What PermDock does not try to do [#what-permdock-does-not-try-to-do]

* Replace authentication: sessions, tokens and users come from Better Auth, Clerk, Supabase, Convex or your own code.
* Require a network call to decide, or a policy language; policies are TypeScript data evaluated in-process. PermDock Cloud is an optional hosted AuthZEN ADS, approval inbox and decision log built on interfaces that ship with in-process defaults.
* Model Zanzibar-scale relation graphs; use OpenFGA or SpiceDB through a provider.
* Issue tokens or verify signatures in core; those live in adapters and the token layer.

See the [roadmap](/docs/roadmap) for what is planned and the [landscape survey](/docs/research/landscape) for the per-library detail and gap matrix these paragraphs are drawn from.

## Sources [#sources]

Facts about the engines added to this page were read from their official documentation, repositories and npm pages on 2026-09-06. The TypeScript libraries are sourced in the [landscape survey](/docs/research/landscape).

* Cedar: [reference guide](https://docs.cedarpolicy.com/), [policy syntax](https://docs.cedarpolicy.com/policies/syntax-policy.html), [schema](https://docs.cedarpolicy.com/schema/schema.html), [cedar-policy/cedar](https://github.com/cedar-policy/cedar), [`@cedar-policy/cedar-wasm` on npm](https://www.npmjs.com/package/@cedar-policy/cedar-wasm).
* Amazon Verified Permissions: [what is AVP](https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/what-is-avp.html), [policy stores](https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/policy-stores.html), [`IsAuthorized`](https://docs.aws.amazon.com/verifiedpermissions/latest/apireference/API_IsAuthorized.html), [`BatchIsAuthorized`](https://docs.aws.amazon.com/verifiedpermissions/latest/apireference/API_BatchIsAuthorized.html), [`IsAuthorizedWithToken`](https://docs.aws.amazon.com/verifiedpermissions/latest/apireference/API_IsAuthorizedWithToken.html).
* Open Policy Agent: [introduction](https://www.openpolicyagent.org/docs/latest/), [REST API](https://www.openpolicyagent.org/docs/latest/rest-api/) (Data API, Compile API), [`@open-policy-agent/opa-wasm` on npm](https://www.npmjs.com/package/@open-policy-agent/opa-wasm).
* OpenFGA: [introduction](https://openfga.dev/docs/fga), [modeling](https://openfga.dev/docs/modeling/getting-started), [relationship queries](https://openfga.dev/docs/interacting/relationship-queries), [conditions](https://openfga.dev/docs/modeling/conditions), [use cases](https://openfga.dev/docs/use-cases), [MCP server authorization](https://openfga.dev/docs/use-cases/mcp-server-authorization).
* Auth0 FGA: [product page](https://auth0.com/fine-grained-authorization) (FAQ: built on OpenFGA), [docs.fga.dev](https://docs.fga.dev/), [Auth0 Lab on Okta FGA](https://lab.auth0.com/experiments/sandcastle).
* SpiceDB: [schema language](https://authzed.com/docs/spicedb/concepts/schema), [protecting a list endpoint](https://authzed.com/docs/spicedb/modeling/protecting-a-list-endpoint).
* WorkOS FGA: [overview](https://workos.com/docs/fga), [roles and permissions](https://workos.com/docs/fga/roles-and-permissions), [access checks](https://workos.com/docs/fga/access-checks), [API reference](https://workos.com/docs/reference/fga), [FGA blog post](https://workos.com/blog/fga-how-workos-is-rethinking-authorization-for-the-next-generation-of-saas).
* Casbin: [overview](https://casbin.org/docs/overview), [how it works](https://casbin.org/docs/how-it-works), [supported models](https://casbin.org/docs/supported-models), [RBAC](https://casbin.org/docs/rbac), [ABAC](https://casbin.org/docs/abac), [node-casbin](https://github.com/apache/casbin-node-casbin), [`casbin` on npm](https://www.npmjs.com/package/casbin).
* accesscontrol: [repository](https://github.com/onury/accesscontrol), [npm](https://www.npmjs.com/package/accesscontrol).
