PermDock
Getting started

Installation

Install the permdock package, pick a Standard Schema validator, and learn which subpath exports exist.

The current release is permdock@0.1.0. See the roadmap for what is planned and how versions are numbered.

Packages

PackagePurposeRequired
permdockCore plus every adapter as a subpath export (permdock/react, permdock/next, permdock/hono, ...), and the permdock binary (collect, catalog, diff, usage, doctor, skills, openapi, rls, arazzo, cloud, supabase)Yes
permdock/testingSubpath of permdock: policy matrix tests, snapshot fixtures, RLS parity runner, conformance runners; Vitest is an optional peerTests only
pnpm add permdock

permdock is the only package. It is published by the scaledockhq npm organisation.

npm and yarn work the same way; the repository itself uses pnpm workspaces.

Requirements

  • Node.js 24 or later for servers and the CLI. The package declares engines.node: ">=24". Browser and React Native bundles are produced from the same ESM entry points and do not depend on Node.
  • TypeScript 7. PermDock is written in and built with TypeScript 7, and the public API avoids template-literal unions so TypeScript 7 stays fast: permissions are typed object references, not string unions. The type tests in tests/types also check the public types under TypeScript 5.9 and 6, so an app that has not moved yet keeps compiling.
  • ESM only. There is no CommonJS build. Your tsconfig.json needs a moduleResolution that understands package exports (bundler, node16 or nodenext) so subpath imports such as permdock/react resolve.
  • Runtime entries depend on @standard-schema/spec only, and it is types only. The package's other dependencies (oxc-parser, citty, jiti, smol-toml, package-manager-detector, @clack/prompts, diff) belong to the CLI and no runtime entry loads them.

Runtimes

Core uses only the WinterTC Minimum Common API (fetch, Request, Response, URL, crypto.subtle, TextEncoder, structuredClone) and no Node built-ins, no AsyncLocalStorage, no module-level state. That is by construction (invariants 7 and 12 in AGENTS.md) and a conformance test in tests/bundle asserts it: every client entry and permdock/server must load in a bare WinterTC-shaped global. The table lists what each runtime adds or lacks; nothing in it changes how a decision is made.

RuntimeCore, permdock/server, agent adaptersNotes
Node.js 24 or laterYesThe CLI and permdock/node need Node; permdock/jwt uses jose's Node build
BunYesElysia's home runtime; bun test runs the policy matrix unchanged
Deno, Deno DeployYesImport from npm specifiers or JSR once published; no Node built-ins to polyfill
Cloudflare WorkersYesmemoryApprovalStore is per isolate: use Durable Objects or KV behind ApprovalStore and SnapshotSource (approvals); D1 has no RLS, where compilers still apply (RLS)
Vercel Functions (Fluid), EdgeYespermdock/next targets the Node runtime for React.cache and 'use cache: private'; the Fetch kernel runs on Edge too
Netlify Functions and EdgeYesSame as Vercel Edge
AWS LambdaYesThrough permdock/node or a Fetch adapter such as Hono's; approvals need an external ApprovalStore
Browsers, React Native (Hermes)Client entries onlypermdock/react, permdock/react-native, permdock/webmcp; no policy code ships (invariant 8)

The one runtime-specific caveat is state: any store or sink that defaults to memory (memoryApprovalStore, memorySink) is per process or per isolate and resets on cold start. permdock doctor warns when the memory store is the configured ApprovalStore in a serverless target. Recipes for Durable Objects, KV, Redis and Postgres stores live on the approvals and audit and observability pages; none is a package.

A Standard Schema validator

Resources are defined from any validator that implements Standard Schema. Bring the one you already use:

pnpm add zod      # or
pnpm add valibot  # or
pnpm add arktype

PermDock never imports a validator itself. resource(Post, ...) reads Post['~standard'] to infer the instance type and to validate untrusted input at trust boundaries (see validation). Boundary validation is synchronous, so a schema whose validate returns a Promise produces a typed PermDockValidationError instead of a silent deny.

Subpath exports

All adapters live in the one permdock package. Import only what a bundle needs; nothing from a server entry is reachable from a client entry.

Import pathContents
permdockdefinePermissions, resource, mergePermissions, listPermissions, findPermission, definePolicy, defineRoles, definePlans, role, allow, deny, principal, context, createPermDock, errors
permdock/reactPermDockProvider, usePermDock, usePermission, <Protected>
permdock/nextcreatePermDock returning getPermDock, getPermission, PermDockProvider, permdockHandler
permdock/next/plugincreatePermDockPlugin (build-time collect hook only)
permdock/unplugincreatePermDockUnplugin (the same collect hook for Vite, Rollup, webpack, Rspack and esbuild; recipes for TanStack Start, React Router, Nuxt, Astro, Effect)
permdock/serverFetch-first kernel shared by the HTTP adapters
permdock/honocreatePermDock returning permdock middleware and protect
permdock/express, permdock/fastify, permdock/elysia, permdock/nest, permdock/nodeSame shape as permdock/hono
permdock/jwtsubjectFromJwt, createJwtSubjectResolver: JWKS verification and claim mapping to a subject (jose optional peer)
permdock/terminalcreatePermDock returning permdock, protect, filterCommands, format for your own CLI
permdock/trpc, permdock/orpcRPC middleware with OpenAPI hooks
permdock/mcpcreatePermDock returning protectServer
permdock/ai-sdkcreatePermDock returning toolApproval, capabilityMiddleware, needsApproval
permdock/claude-agentcreatePermDock returning canUseTool, permissionRequestHook
permdock/evecreatePermDock returning approval, approvalFor, permdock for Eve defineTool
permdock/openaicreatePermDock returning needsApproval, guardTools, resolveInterruptions, permdock for the OpenAI Agents SDK
permdock/approvalsApprovalStore interface, memoryApprovalStore, approvalsHandler; the store option of every agent and HTTP adapter
permdock/cloudcloud({ url, key }) returning approvals, sink, snapshots for PermDock Cloud; optional, server-only, never on the decision path
permdock/webmcpregisterTools
permdock/a2acreatePermDock returning agentCard, extendedAgentCard
permdock/authzencreatePermDock returning the AuthZEN permdockHandler
permdock/ssfcreatePermDock returning the CAEP receiver
permdock/scimscimHandler, the DirectoryStore interface, memoryDirectoryStore, directoryMembershipSource
permdock/openapiOpenAPI emission (document or Overlay) and import
permdock/otelSpan per check and a decision counter
permdock/react-nativeReact adapter plus persisted storage
permdock/vue, permdock/svelte, permdock/solidUI adapters
permdock/drizzle, permdock/prisma, permdock/kyselytoWhere condition compilers
permdock/supabase, permdock/better-auth, permdock/clerk, permdock/convex, permdock/pdpSubject providers

The full matrix with example apps and related standards is on the adapters index.

Optional peers

Runtime entries have no dependency beyond @standard-schema/spec. A few entries verify material or talk to a vendor SDK and declare an optional peer instead of bundling it, and a few CLI commands do the same:

EntryOptional peerWhy
permdock/jwtjoseJWS verification and JWKS fetching; see JWT
permdock/otel@opentelemetry/apiSpan and metric emission
permdock/supabase, permdock/clerk, permdock/better-auth, permdock/convexThe provider SDK, at the call siteThe adapters take the values the provider's own SDK returned and declare no peer themselves; session and claim verification stays with the provider; see authentication
permdock/terminalAn OS keychain bindingToken storage; falls back to a mode-0600 file
permdock/testingvitestdescribePolicy and the runners register Vitest tests
permdock rls importpgsql-parserParses Postgres policies (WASM, about 2.8 MB installed); the command prints the install line when it is missing
permdock rls import --db, permdock rls verify --dbpgTalks to a live database
permdock/unpluginunpluginThe bundler adapter layer for Vite, Rollup, webpack, Rspack and esbuild

Why runtime entries have no dependencies

Core runs in browsers, React Native, edge functions, serverless functions, MCP servers and Node, and every dependency it carries is paid by all of them. So every runtime entry imports exactly one package, @standard-schema/spec. It is types only, and it is a real dependency rather than vendored types because resource definitions take a StandardSchemaV1, which makes it part of the public API that should version with the spec.

  • No validator. A bundled Zod would lock out Valibot and ArkType users and add tens of kilobytes to clients that never call it.
  • No serialiser. Conditions and snapshots are plain JSON with tagged ISO date strings, one format that works in Postgres, snapshots and catalogs, so there is no superjson.
  • No telemetry. OpenTelemetry lives in permdock/otel behind a type-only Tracer interface rather than as a required peer.
  • ESM only. The target runtimes are ESM, and a dual CommonJS build would double the test matrix and bring the dual-package hazard.
  • Tooling never reaches a runtime entry: the CLI and the test runners ship in the same package, but no runtime entry imports them. The one CLI dependency, oxc-parser (about 3.5 MB installed with one platform binding), is what collect, doctor and the build hooks need; yaml and ajv are bundled into the lazily loaded OpenAPI command chunks, and the Postgres parser, pg, unplugin and Vitest are optional peers.

There is no size cap chosen in advance. tests/bundle measures the min+gzip size of every entry, and the measured size is the regression baseline: a test compares every entry against the recorded baseline, so a size change fails CI until the baseline is updated in the same PR, where review sees it. Core stays small because of the rules above, not because of a number picked from a competitor's bundle, so features such as the condition AST or the Decision shape are never cut to meet a guess.

TypeScript configuration

A minimal tsconfig.json that works with the subpath exports and keeps inference fast:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "exactOptionalPropertyTypes": true,
    "skipLibCheck": true
  }
}

strict matters: subject narrowing through assert and the actions / collection arity checks rely on strict null checks. The library itself is built with isolatedDeclarations and erasableSyntaxOnly, so its declaration files contain no inferred-only types and load quickly.

Where files go

FileContentsImported by
src/permissions.tsdefinePermissions() or mergePermissions(); no rulesEverything, including client bundles
src/policy.tsdefinePolicy() with rolesServer only
src/permdock/server.tscreatePermDock from a server adapterServer code
src/permissions.generated.tsOutput of permdock rls import or openapi importsrc/permissions.ts
permissions.catalog.jsonOutput of permdock collect; committedCI (--check), docs, OpenAPI

The quick start fills in the first three.

Verify the install

pnpm permdock doctor

doctor checks the TypeScript version, module resolution, that exactly one copy of permdock is installed, that no server entry is imported from a client file, that memoryApprovalStore() is not the store in a serverless deployment that surfaces approvals, that PERMDOCK_CLOUD_* variables (when present) are server-only and reach the API, and that the catalog produced by permdock collect is fresh. The permdock-wire skill runs it as soon as the three files exist.

Next steps

  • Quick start: define, grant, check, guard.
  • Larger apps: per-feature definitions, mergePermissions, permdock collect.
  • Naming: why every adapter exports createPermDock.

Last updated on

On this page