# Authorization landscape

Source: https://permdock.com/docs/research/landscape

Prior art for PermDock. In-process TypeScript libraries (CASL, permix, Kilpi, @zap-studio/permit, accesscontrol), external policy engines (Cerbos, OpenFGA, SpiceDB, Cedar, OPA, casbin), authorization bundled with auth providers and data layers, and the commercial vendor landscape, each with what PermDock adopts, adapts and avoids.

Versions, star counts and prices are as surveyed in September 2026 from npm, GitHub, vendor docs and pricing pages. They drift; the structure of each design is the finding. Claims that could not be confirmed are marked unverified. Every tool named here has a row on the [ecosystem index](/docs/research/ecosystem-index).

## Summary [#summary]

Two families dominate. In-process TypeScript libraries (CASL, permix, Kilpi, accesscontrol, Better Auth's `createAccessControl`) infer keys well but mostly stop at booleans, with CASL the only one that reaches the data layer. External engines (Cerbos, Permit.io, OpenFGA, SpiceDB, Oso Cloud, casbin, Cedar, OPA) are strings in and booleans out with almost no TypeScript inference.

| Capability | CASL | permix | Kilpi | accesscontrol | Better Auth | Cerbos | OpenFGA | ZenStack | zap/permit |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Keys inferred from the 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 a `where` clause | yes (Prisma, Mongoose) | no | no | no | no | yes (query plan) | partial (`ListObjects`) | yes (compiled SQL) | no |
| Field-level | yes | no | partial (redact) | yes | no | partial | no | yes | no |
| React, Next RSC, React Native | partial | partial | partial | no | partial | partial | no | no | no |
| Vue, Svelte, Solid | Vue, Angular | yes | no | no | no | no | no | no | no |
| Server framework adapters | no | yes | partial | no | no | no | no | partial | no |
| SSR hydration | partial (pack) | yes (booleans) | partial | no | no | no | no | no | no |
| Explain why denied | partial (`reason`) | no | no | partial (events) | no | partial (audit) | partial (`Expand`) | no | no |
| 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 | no | no | yes | yes | yes | no | yes |

Gaps nobody fills well, and PermDock's answer:

1. **Standard-Schema-native definitions.** Only `@zap-studio/permit` consumes `~standard`. PermDock's `resource(Schema, ...)` types conditions on the schema's output and exports a JSON Schema catalog.
2. **MCP tool authorization.** The SDK gives `authInfo.scopes` and `scopeChallenge`; commercial options are proxies. PermDock binds `permission` on `registerTool`, filters `list_tools` and derives scope challenges ([MCP adapter](/docs/adapters/mcp)).
3. **One snapshot for React, React Native and Next.js.** Nobody ships it; PermDock's snapshot feeds `permdock/react`, `permdock/react-native` and `permdock/next`.
4. **Non-blocking page checks.** Nobody streams decisions. `usePermission()` returns `allowed` and `status`, and `Protected` renders the shell immediately ([Next.js Cache Components](/docs/guides/next-cache-components)).
5. **OpenAPI emission.** Every generator exposes a hook; no authorization library fills it ([OpenAPI adapter](/docs/adapters/openapi)).
6. **Structured decisions.** CASL's `reason` plus `relevantRuleFor` is the high-water mark. A decision with the matched grant, denials, alternatives and a `simulate()` pre-flight is uncontested ([decisions](/docs/concepts/decisions)).
7. **Conditions to SQL without ORM lock-in.** CASL, Cerbos, ZenStack, Oso Cloud and `@typed-policy/core` each do one flavour. PermDock compiles one Standard-Schema-typed AST to Drizzle, Prisma, Kysely and RLS and evaluates it on the client ([conditions](/docs/concepts/conditions), [RLS adapter](/docs/adapters/rls)).

## In-process TypeScript libraries [#in-process-typescript-libraries]

### CASL [#casl]

`@casl/ability` 7.0.1, `@casl/react` 7.0.1, `@casl/prisma` 2.0.2; MIT, about 7.1k stars, dual CJS and ESM, about 6.2 kB gzip core. CASL is the only mature in-process library that compiles conditions to ORM `where` clauses and supports field-level rules, so its internals are the best reference for a rule index and a portable AST, and its public API is the best list of things not to repeat.

* **Rules.** Plain JSON `action`, `subject`, MongoDB-style `conditions` (via `@ucast`), glob `fields`, `inverted`, `reason`. A `Rule` wraps each with `priority` and `origin` and compiles matchers lazily. `RuleIndex` maps subject type to action to a priority-sorted bucket, merges the `manage` and `all` wildcard buckets once and caches the frozen result, so "last rule wins" is "first match".
* **Conditions.** A parser produces a `@ucast/core` AST (`FieldCondition`, `CompoundCondition`) that feeds a JavaScript interpreter and the Prisma and Mongo builders. `rulesToCondition` turns rule priority into boolean algebra: each conditional allow is ANDed with every higher-priority conditional deny, an unconditional rule stops the walk, and `null` means nothing is allowed. Before this, the runtime check and the database query could disagree.
* **Prisma.** `accessibleBy(ability, 'read').ofType('Post')` returns a `WhereInput`; an empty result becomes `{ OR: [] }`, and `createCaslExtension()` rewrites it so every operation returns no rows (fail closed).
* **Fields.** `*` and `**` patterns with lazy regex compilation. `permittedFieldsOf` needs a hand-written `fieldsFrom` because CASL does not know the schema, and it returns wildcard patterns verbatim.
* **Typing.** `MongoAbility<[Actions, Subjects]>` is declared, not inferred. Subject detection relies on `subject('Post', obj)` tagging the user's object or on `constructor.name`, which breaks under minification. Per-subject condition types use a homemade higher-kinded-type encoding; nested paths need hand-flattened types.
* **Explain.** `ForbiddenError.from(ability).throwUnlessCan(...)` and `relevantRuleFor` explain only a matching inverted rule; default deny has no explanation.
* **React.** `useAbility` subscribes with `useSyncExternalStore` (the right pattern). `Can` takes English-sentence props (`I`, `do`, `on`, `this`) with poor type errors. An `Ability` instance cannot cross the RSC boundary.
* **Serialisation.** `packRules` produces an undocumented compact format. CASL rules hold concrete values (`authorId: user.id`), so they cannot be stored per role or compiled to RLS; generic SQL support has been open since 2017.

Adopt: rules as plain JSON with lazy compilation; a priority-sorted, cached index; one portable AST feeding the in-memory check and every `where` builder; the `rulesToCondition` flattening with its `null` short-circuit and fail-closed empty result; `reason` on rules; `useSyncExternalStore` subscription; `*` and `**` field patterns.

Adapt: drop subject detection, because `can(permissions.post.update, post)` already names the resource, and validate at boundaries instead of tagging objects. Derive condition and field types from the schema's inferred output. Make field lists schema-aware. Make type versus instance explicit (`actions` take an instance, `collection` actions do not). Carry immutable JSON snapshots across boundaries instead of mutating with `update()`. Reference `principal.id` symbolically so conditions bind at evaluation or compile time. Ship a versioned snapshot format.

Avoid: string-tuple generics, `InferSubjects`, `ForcedSubject`, mutating user objects and `constructor.name`; one name (`can`) for defining and checking, and positional overloads; conditions in any dialect (Mongo or Prisma `WhereInput`) instead of one canonical dialect; function conditions as a first-class path; class instances as the unit of state.

### permix [#permix]

`permix` 4.1.2; MIT, about 620 stars, ESM-only, Node 22+, 2.64 kB gzip core with zero dependencies. The closest library to PermDock in adapter breadth: one package with subpath adapters for React, Vue, Solid, Svelte, Next.js, TanStack Start, Node, Express, Hono, Fastify, Elysia, tRPC, oRPC, Effect and Drizzle. It serves `llms.txt` and ships agent skills.

* **Model.** Permissions are only a TypeScript generic (`createPermix<{ post: ['create', 'read'] }>()`); nothing exists at runtime, so there is no catalog and no payload validation. Keys are template-literal strings (`'post.edit'`).
* **Runtime.** A mutable closure: `setup(rules)` replaces the tree, `check()` throws before setup, and every server adapter works around the shared instance by creating one per request. `dehydrate()` collapses function rules to booleans, so the client must call `setup()` again; Solid, Svelte and Vue had first-render bugs.
* **Data.** The Drizzle adapter only derives resource keys from table names; there are no `where` clauses.
* **Tracker lessons.** Global `setup()` leaked permissions between concurrent requests. Denial reasons were requested and pointed at `onForbidden`; the audit hook gained only the path, not the outcome. Real ReBAC was answered with closures. Server-only imports inside a TanStack Start middleware callback leaked Prisma and Better Auth into the client bundle. Published `.mjs` contained raw JSX. Middleware generics were untyped or typed against the raw framework context. `llms.txt` briefly returned 404. Users asked for an HTTP PDP so Go and Rust services could call TypeScript policy, and for NestJS and Convex integrations.

Adopt: one package with subpath exports; zero runtime dependencies in core with measured per-entry size; skills in the package and `llms.txt` on the docs; per-request instances on the framework's request context; tsdown, Oxlint, Oxfmt and Turborepo.

Adapt: the Next.js per-request instance memoised with React `cache()` becomes `getPermDock()` in `permdock/next`. Type-only definitions become `resource(Schema, ...)`. The definition is already the catalog, and `permdock collect` scans usages. Boolean `check()` becomes one `decide()`. Adapter-by-adapter request isolation becomes a Fetch-first server kernel with thin adapters. The HTTP PDP request becomes `permdock/authzen`.

Avoid: type-only definitions; a mutable singleton or a global `ready` flag; boolean-only hydration; string paths as the public API; audit hooks without the outcome; untyped adapter generics; publishing without `publint` and `arethetypeswrong`.

### Kilpi [#kilpi]

`@kilpi/core` 1.1.3 with client and React packages; MIT, 89 stars, one maintainer, no commits since December 2025. Server-first: a policy is a function of the subject and an optional object returning `Grant(subject)` or `Deny({ message, reason, metadata })`, reached through a fluent proxy (`Kilpi.posts.delete(post).authorize()`).

* **Addressing.** The path-addressed tree is excellent DX but is built with a `Proxy` and a heavy recursive mapped type, and the same string path is the cache key, audit key and endpoint key. Non-policy members are `$`-prefixed to avoid collisions; `Reflect.has` also sees the prototype chain.
* **Narrowing.** `Grant` is generic over its argument, so after `if (!subject) return Deny()` the granted decision carries a non-nullable subject. The narrowing is lost on the client, in the RSC component and in hook events.
* **Unauthorised handling.** `assert()` runs the per-call handler, every hook and the global handler, then rethrows the first error.
* **Subject.** `getSubject(ctx?)` runs on every check unless a cache hook answers. Kilpi removed AsyncLocalStorage because it broke some runtimes; the RSC plugin caches with `React.cache`.
* **Protected queries.** `$query(fn, { authorize })` co-locates redaction with data fetching, post-fetch only.
* **Client.** A batched, deduplicated decision endpoint with prefix cache invalidation. The endpoint accepts an unvalidated `object: z.any()`, authenticates with a public secret, and ignores a declared `getContext` option. `useAuthorize` does not refetch when the input object changes. Core hard-depends on `zod` and `superjson`.

Adopt: a discriminated `Decision` carrying `message`, `reason` and `metadata`; argument-inferred subject narrowing; layered unauthorised handlers that all run before the first error is rethrown; a lazy subject with cache hooks and no AsyncLocalStorage in core; inferring the server type on the client; `Pending`, `Unauthorized`, `Error` and `Idle` render states.

Adapt: materialise the path tree eagerly as plain JSON leaves with a `key`, so no `Proxy` or `$` prefix is needed. Key client caches on the schema-declared resource id, and refetch when it changes. Keep narrowing through `assert`, RSC and client adapters. Encode arity in the leaf type. Scope plugin typing to the instance.

Avoid: hard `zod` and `superjson` dependencies; `$`-prefixed members; an unvalidated decision endpoint or a public-secret "auth"; global module augmentation for plugin types; RSC-detection hacks.

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

`@zap-studio/permit` 2.0.1; MIT, ESM. The only authorization library built on Standard Schema: `createPolicy({ resources, actions, rules })` where resources are Standard Schema validators and rule parameters are typed by `InferOutput`.

* **Evaluation.** One function per action (`allow()`, `deny()`, `when(cond)`), so there is no deny precedence inside a policy. `can()` validates the resource on every call, never throws, and treats a missing rule, invalid input or a rejected sub-policy as deny. `mergePoliciesAnd` and `mergePoliciesOr` use `Promise.allSettled`.
* **Limits.** Rules are synchronous behind an async API, so an `async` rule silently always denies. Permissions are runtime-parsed strings (`"post:write"`). A resource object is required even for `create`. Action maps are `Partial`, so a forgotten rule compiles and denies. Results are boolean only. Splitting rules across files needs three manual generics. `@opentelemetry/api` is a required peer. There are no adapters, no subject concept and no introspection.

Adopt: resources as Standard Schema validators feeding condition types; fail-closed evaluation everywhere; `Promise.allSettled` composition; a tiny footprint; a structural, type-only logger; one OpenTelemetry span per check with a decision attribute; a long test matrix on malformed input.

Adapt: validate only data that crossed a trust boundary, not trusted server rows. Keep the `allow`, `deny`, `when`, `and`, `or`, `not` vocabulary but make conditions portable data, async-capable through `context`. Key every grant under a permission reference so `permdock usage` reports a permission no role grants.

Avoid: boolean results; sync rules behind async APIs; runtime string parsing; required resource objects for collection actions; `Partial` rule maps; a required OpenTelemetry peer; manual generics to split policies.

### accesscontrol [#accesscontrol]

`accesscontrol` 3.1.0; MIT, 2.3k stars, ESM, two runtime dependencies. Chainable role grants (`ac.grant('user').createOwn('video')`) with `own` versus `any` possession, `.extend()` inheritance with deny-overrides, and a v3 condition engine (`.where('$.order.value <= 100000')`, operators including `in`, `matches`, `cidr` and schedules) stored as canonical JSON. `require()` gates can only restrict and fail closed on missing context. Glob attribute lists and `filter()` handle field-level output; `tryCan()` never throws; `snapshot()` and `restore()` persist grants; name handling is prototype-pollution-safe. Roles, resources and actions are runtime strings with no inference, conditions evaluate only in memory, and there is no client snapshot, UI, OpenAPI, MCP or AuthZEN.

Adopt: `tryCan()` and restrict-only `require()` gates as independent confirmation that fail-closed and deny-overrides are the right defaults. Avoid: runtime-string names and in-memory-only conditions.

### Smaller libraries [#smaller-libraries]

* `@rbac/rbac` 2.2.2: small RBAC with inheritance and async `can()`.
* `role-acl` and `permissio`: unmaintained since 2022.
* `@typed-policy/core` 0.4.0: one policy AST with `evaluate()` on the client and `compileToDrizzle()`. The "one definition for SQL and UI" idea, tiny and unproven.
* `@permx/core` 0.4.0: RBAC stored in Mongoose or Prisma with a headless React SDK.

## External policy engines [#external-policy-engines]

* **Cerbos.** Apache-2.0 PDP (sidecar, Hub or embedded WASM) evaluating YAML policies with CEL conditions. The SDK is typed on the protocol, not on your resources. It has query plans to Drizzle, Prisma and Mongoose, OpenTelemetry, audit logs, policy tests and AuthZEN.
* **Permit.io.** An OPA and OPAL-backed PDP behind `permit.check(user, action, resource)`. Its MCP Gateway proxies agent traffic and classifies tools by name.
* **OpenFGA and Auth0 FGA.** CNCF Zanzibar ReBAC; Auth0 FGA is the managed service built on it. A model DSL plus relationship tuples, with CEL conditions on tuples. The API is `Check`, `BatchCheck`, `Expand`, `ListObjects` and `ListUsers`. Filtering returns ids for `WHERE id IN (...)`. The MCP guide models tools as objects with a `can_call` relation, and agents as their own principals.
* **SpiceDB (AuthZed).** Zanzibar ReBAC with a `.zed` schema (relations, computed permissions, arrows, CEL caveats, expiring relationships). The list-endpoint guide names three strategies: `LookupResources` ids into `WHERE id = ANY(...)` for small sets (under about 10,000), `CheckBulkPermissions` over candidate pages, and a materialised local permission set.
* **WorkOS FGA.** Hierarchical, resource-scoped RBAC with no DSL. Resource types live in the dashboard, and roles on a parent propagate to children. Organisation-wide checks read AuthKit tokens; resource-scoped checks call the API. It has no conditions.
* **Oso.** The embeddable library is deprecated in favour of Oso Cloud (Polar over facts), whose `listLocal` returns SQL fragments. "Oso for Agents" is monitoring and a proxy.
* **casbin.** The PERM metamodel (request, policy, effect, matcher) in a `.conf` file with CSV or database policies. Deny-override is one effect expression among several, and RBAC with domains is a documented model. Names are strings, `enforce` returns a boolean, and parity across many languages is its strength.
* **Cedar and Amazon Verified Permissions.** `permit` and `forbid` policies with `when` and `unless`, where any `forbid` wins and no `permit` means deny. Policies are validated against a schema at authoring time, and a symbolic compiler proves properties. AVP adds hosted policy stores (one per application or tenant), `IsAuthorized`, `BatchIsAuthorized` and `IsAuthorizedWithToken`, returning `determiningPolicies`. Entity literals are strings at the call site.
* **Open Policy Agent.** Rego over arbitrary JSON, served as a REST daemon, a Go library or WASM. The Compile API's partial evaluation emits data filters (`ucast+prisma`, `sql+postgresql`), the closest any external engine gets to a `where` compiler. Deny-override is the policy author's convention. `@ai-sdk/policy-opa` evaluates Rego in AI SDK tool approvals and fails open on unrecognised decisions ([vercel/ai#19978](https://github.com/vercel/ai/issues/19978)).

None of these engines has an approval outcome; each answers allow or deny, which is why `approval-required` is a PermDock concept rather than something mapped from a PDP. OpenFGA and SpiceDB model agents as separate principals, and PermDock's `actor` plus `delegation` intersection is the in-process counterpart, forwarded to remote PDPs in `subject.properties`.

Adopt: Cedar's authoring-time validation, mirrored by `permdock collect --check` and `permdock doctor`, and its practice of returning the deciding policies, mirrored by `Decision.matched` and `denials`. Take Cerbos's embedded WASM PDP as the motivation for a client snapshot that answers offline.

Adapt: bridge OpenFGA and SpiceDB graphs through `permdock/pdp` instead of reimplementing them. Their `ListObjects` and `LookupResources` become the `filter` of the `pdp` presets, with `where` compiling to `in(row.id, ids)`. OPA's Compile API is the bar `permdock/drizzle`, `prisma` and `kysely` must meet in-process. AVP's single, batch and token-authenticated shapes are what `permdock/authzen` exposes over AuthZEN. Casbin's PERM split is a checklist: the `Decision` and catalog make request, policy, matcher and effect visible for audit.

Avoid: strings-in, boolean-out SDKs as the primary API; DSLs that need their own compiler (Polar, Cedar, Rego) as the authoring format; configurable deny-override or fail-open conventions; general userset rewrites and unbounded recursive membership in the portable AST. The bounded subset (one `related` node covering parent chains, role-filtered edges, implied relations, groups nested at most 16 deep and to-one links) is adopted, because each part compiles to SQL ([relationships](/docs/concepts/relationships)).

## Authorization bundled with providers and data layers [#authorization-bundled-with-providers-and-data-layers]

* **Better Auth.** `createAccessControl(statement)` with keys inferred from an `as const` statement, then `ac.newRole()`, used by the `organization` and `admin` plugins. RBAC without conditions; dynamic roles are stored in the database. PermDock consumes these roles through [`permdock/better-auth`](/docs/adapters/better-auth).
* **Clerk.** Organisation-scoped custom permissions (`org:invoices:create`), `has()` returning a boolean, `protect()` and `Protect`. Types come from interface augmentation, and there are no conditions and no data layer.
* **Auth.js.** No authorization primitives: copy `role` through the `jwt()` and `session()` callbacks and gate in `authorized()`.
* **ZenStack v3.** ZModel `@@allow` and `@@deny` on models and fields, with deny winning, compiled to SQL through Kysely. v3 dropped the database-free `check()`, so the UI cannot ask "can I?".
* **Keel and Nile.** Keel puts `@permission` expressions on models (maintenance unverified). Nile provides tenant isolation in virtualised Postgres rather than a permissions library.
* **Payload, Directus, Strapi.** Payload access functions return `boolean` or a `Where`, and it ships Claude plugin skills with an access-control reference. Directus policies are data (`permissions` and `validation` filters, `fields`). Strapi conditions return a boolean or a sift query.

Adapt: providers' roles are subject input, not competitors. ZenStack's compiled policies become `toWhere` compilers plus RLS export with parity tests, keeping the database-free `can()` ZenStack dropped. Avoid: vendor-locked permissions and interface augmentation for types.

## Commercial landscape [#commercial-landscape]

### Vendors and pricing [#vendors-and-pricing]

Every surviving open-core vendor gives the engine away and charges for the operational layer around it.

| Vendor | Free | Paid layer | Meter |
| --- | --- | --- | --- |
| Cerbos | Apache-2.0 PDP, no caps | Hub: policy distribution, audit store, enrichment | Monthly active principals (free, then about 25 USD per month to about 933 USD per month) |
| Permit.io | Up to 1,000 MAU and 20 tenants | UI, sync, MCP Gateway | MAU steps |
| AuthZed | SpiceDB | Hosted clusters | About 2 USD per cluster hour |
| Oso | (library deprecated) | Oso Cloud, Oso for Agents | About 15 USD per user per month |
| WorkOS FGA | none | Inside the WorkOS bundle | About 150 USD per month |
| Arcade, Composio | none | Hosted tool-auth proxies | About 25 to 29 USD per month plus per call or per authorization |
| Pushary, Rills | Rills free | Durable approval API with audit log | Flat about 99 USD per month (Pushary) |

None of them sells typed permission references, one condition compiled to UI, SQL and RLS, `approval-required` as a first-class outcome, or a decision event naming principal, actor and delegation.

### Where the market is going [#where-the-market-is-going]

* **Standalone authorization rarely survives alone.** Aserto shut down after running a control plane over customer-side authorizers. Styra's team went to Apple. Warrant was absorbed into WorkOS. Oso deprecated its library. The survivors are small, and Cerbos's own path from PDP to an enterprise management platform shows that compliance buyers pay for the control plane, not developers.
* **Two buyers pay.** Enterprise procurement pays for SSO, SCIM and audit evidence, and security teams pay to govern agents. Both buy "who can do what, prove it, and let me stop it". PermDock already produces the two artefacts they need: a decision event carrying `principal`, `actor`, `delegation`, `tenant` and `outcome`, and a bound approval token.
* **Approvals are commoditising.** Pushary, Rills, intrupt, Sesame and AxonFlow sell or give away approval APIs. What is not commoditised is the token rule: re-run `decide` and recompute the bound `token` before trusting a stored approval, and check that the approver is not the actor.
* **Auth providers are moving into agent identity, not application permissions.** Vercel owns Better Auth (Agent Auth: scoped, revocable delegated identities via RFC 8693). Auth0 sells Token Vault and CIBA. Clerk's Eve integration explicitly leaves tool-call authorization to the application. Okta Cross App Access and Microsoft Entra Agent ID issue tokens whose `sub` is the human and whose client is the agent. MCP authorization servers (Stytch, Descope, Scalekit, WorkOS Connect, Supabase's OAuth 2.1 server) issue the tokens `permdock/mcp` consumes. All of them are token sources for `subjectFrom*`, never rivals.
* **Organisation RBAC is the B2B upsell.** Clerk, Auth0 Organizations, WorkOS, Kinde, Frontegg, PropelAuth and Descope sell memberships per organisation, a role set and sometimes teams. None expresses a row condition or reaches a database policy ([tenancy](/docs/concepts/tenancy)).
* **Agent tool authorization is funded, and it is proxies and gateways.** Arcade, Composio, Permit's MCP Gateway, Oso for Agents and more than a dozen MCP-gateway entrants see tool names and arguments as strings. Amazon Bedrock AgentCore Policy evaluates Cedar over `context.toolName` and `context.input.*` at the gateway, but it has no notion of the row and no human-approval outcome. A gateway in the decision path would be exactly the strings-in, boolean-out product PermDock is positioned against. Identity-aware proxies (Pomerium, Cloudflare Access) are won by whoever owns the network path.
* **Every agent runtime pauses and leaves the record to the application.** Eve splits `approval` and `approval.response` (with an authenticated responder), the OpenAI Agents SDK serialises `RunState` so approvals can wait for days, and AI SDK 7, the Claude Agent SDK and MCP elicitation follow the same shape. The store, approver identity, expiry and resume proof are the same problem each time, which is why `ApprovalStore` is an interface. Delivery (Vercel Chat SDK `requestApproval`, durable runtimes, n8n) is where the products are ([approvals adapter](/docs/adapters/approvals)).
* **AuthZEN is final, and the gateways speak it.** Kong, Envoy `ext_authz`, Tyk, Zuplo and WSO2 act as AuthZEN enforcement points. A hosted PermDock endpoint therefore serves teams with no SDK at all: author policy in TypeScript, enforce it from a Go service or a gateway ([AuthZEN](/docs/standards/authzen)).
* **Observability and compliance.** LLM observability tools ingest OpenTelemetry GenAI spans, SIEMs ingest OCSF, and Vanta and Drata collect access-review evidence. Each is a destination for decision events, never a wrapped integration ([audit and observability](/docs/concepts/audit-and-observability)).

### Vercel Marketplace [#vercel-marketplace]

A native listing needs an integration server implementing the Partner API (installation upsert and delete through Vercel SSO, products and plans, resource provisioning, billing data at least daily, invoice submission, `invoice.notpaid` handling with a 15-day grace period). It also needs a provisionable resource, SSO into the dashboard, usage charts, a Getting Started guide and a deploy template. PermDock Cloud provisions one environment per Vercel project and environment, writes `PERMDOCK_CLOUD_URL` and a server-only `PERMDOCK_CLOUD_KEY`, and uses `apps/examples/eve-agent` as the template. The Supabase partner marketplace is a second channel ([Cloud adapter](/docs/adapters/cloud)).

### Positioning [#positioning]

Authentication says who; PermDock says what they may do, for which row, and whether a human must confirm. The engine is MIT and complete. PermDock Cloud is a hosted Authorization Decision Service and control plane: decision log and evidence first, then SCIM relay, approval inbox and AuthZEN endpoint, all optional and none on the decision path ([Cloud adapter](/docs/adapters/cloud)). The meters are monthly active principals (each agent `actor` counted once), connected tenants, resolved approvals above a free allowance, retention tiers and hosted evaluations. Decisions made by the embedded engine are never metered.

Set aside: an MCP proxy or permissions gateway; paid adapters or a source-available core; a login SDK or a network-path identity or MCP gateway; a Cloud-only SCIM receiver, or a Cloud `MembershipSource`; a first-party GitHub Action (two `run:` lines cover it); a Cedar compile target, which is feasible because the combining rule is identical but is not scheduled; renaming the brand.

Adopt: Cerbos's split of a free complete engine and a paid operational layer; the PDP and ADS vocabulary; AuthZEN for the hosted endpoint; Vercel Chat SDK `requestApproval` as the reference delivery recipe; OCSF and the OTel `execute_tool` span as the two projections that make every observability vendor a destination; the decision log as the lead paid capability, with every event written by default; standard wire formats so the Cloud ports into any stack.

Adapt: Cerbos Hub's audit store becomes a `DecisionSink`, so self-hosters get the same records. Proxies' approval features become a per-tool binding plus an `ApprovalStore`. Provider agent identity becomes `actor` and `delegation` through `subjectFrom*`. Clerk Billing `pla` becomes `principal.plans`, while `fea` stays on roles.

Avoid: any design where `decide` needs the Cloud; relicensing the core; metering embedded decisions; sampling `granted` events by default; owning the approver identity; per-vendor packages for delivery channels, sinks or identity providers.

## Toolchain notes [#toolchain-notes]

* **Standard Schema.** `@standard-schema/spec` 1.1.0 is implemented by Zod, Valibot, ArkType, Effect Schema, yup, joi, typia, Mongoose, VineJS and others, and consumed by tRPC, TanStack, Hono, Elysia, oRPC and React Hook Form. PermDock installs it as a regular dependency because it is part of the public API ([Standard Schema](/docs/standards/standard-schema)).
* **TypeScript 7.** The native compiler ships without a stable programmatic API. PermDock uses `isolatedDeclarations` and `erasableSyntaxOnly`, keeps permission keys bounded and avoids deep recursive conditional types, and derives codegen from runtime definitions or Standard JSON Schema, never the compiler API.

## Adopt / adapt / avoid [#adopt--adapt--avoid]

Adopt: one package with subpath exports and zero runtime dependencies in core; Standard Schema resources; one portable condition AST for memory, ORM and RLS; fail-closed evaluation and deny-overrides as invariants; structured decisions naming what decided; agent-readable docs and shipped skills.

Adapt: provider roles, relationship engines and hosted PDPs as inputs and bridges (`subjectFrom*`, `permdock/pdp`, AuthZEN), never reimplemented; commercial operational layers as interfaces with in-process defaults.

Avoid: strings-in, boolean-out APIs; type-only definitions or declared tuples as the typing strategy; required runtime dependencies in core; vendor-locked permissions and DSLs as the authoring format; relying on the TypeScript compiler API; a gateway in the decision path.
