PermDock
Research

Authorization 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.

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.

CapabilityCASLpermixKilpiaccesscontrolBetter AuthCerbosOpenFGAZenStackzap/permit
Keys inferred from the definitionpartial (declared tuple)yesyespartialyes (as const)nonon/a (DSL)yes
Standard Schema resourcesnonononononononoyes
Conditions to a where clauseyes (Prisma, Mongoose)nonononoyes (query plan)partial (ListObjects)yes (compiled SQL)no
Field-levelyesnopartial (redact)yesnopartialnoyesno
React, Next RSC, React Nativepartialpartialpartialnopartialpartialnonono
Vue, Svelte, SolidVue, Angularyesnonononononono
Server framework adaptersnoyespartialnonononopartialno
SSR hydrationpartial (pack)yes (booleans)partialnononononono
Explain why deniedpartial (reason)nonopartial (events)nopartial (audit)partial (Expand)nono
OpenAPI security emissionnonononononononono
MCP tool bindingnonononononopartial (guide)nono
llms.txt or skillsnoyesnonoyesyesyesnoyes

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).
  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).
  5. OpenAPI emission. Every generator exposes a hook; no authorization library fills it (OpenAPI adapter).
  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).
  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, RLS adapter).

In-process TypeScript libraries

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 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/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-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 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

  • @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

  • 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).

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).

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.
  • 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

Vendors and pricing

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

VendorFreePaid layerMeter
CerbosApache-2.0 PDP, no capsHub: policy distribution, audit store, enrichmentMonthly active principals (free, then about 25 USD per month to about 933 USD per month)
Permit.ioUp to 1,000 MAU and 20 tenantsUI, sync, MCP GatewayMAU steps
AuthZedSpiceDBHosted clustersAbout 2 USD per cluster hour
Oso(library deprecated)Oso Cloud, Oso for AgentsAbout 15 USD per user per month
WorkOS FGAnoneInside the WorkOS bundleAbout 150 USD per month
Arcade, ComposiononeHosted tool-auth proxiesAbout 25 to 29 USD per month plus per call or per authorization
Pushary, RillsRills freeDurable approval API with audit logFlat 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

  • 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).
  • 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).
  • 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).
  • 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).

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).

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). 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

  • 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).
  • 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: 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.

Last updated on

On this page