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
| Package | Purpose | Required |
|---|---|---|
permdock | Core 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/testing | Subpath of permdock: policy matrix tests, snapshot fixtures, RLS parity runner, conformance runners; Vitest is an optional peer | Tests only |
pnpm add permdockpermdock 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/typesalso 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.jsonneeds amoduleResolutionthat understands packageexports(bundler,node16ornodenext) so subpath imports such aspermdock/reactresolve. - Runtime entries depend on
@standard-schema/speconly, 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.
| Runtime | Core, permdock/server, agent adapters | Notes |
|---|---|---|
| Node.js 24 or later | Yes | The CLI and permdock/node need Node; permdock/jwt uses jose's Node build |
| Bun | Yes | Elysia's home runtime; bun test runs the policy matrix unchanged |
| Deno, Deno Deploy | Yes | Import from npm specifiers or JSR once published; no Node built-ins to polyfill |
| Cloudflare Workers | Yes | memoryApprovalStore 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), Edge | Yes | permdock/next targets the Node runtime for React.cache and 'use cache: private'; the Fetch kernel runs on Edge too |
| Netlify Functions and Edge | Yes | Same as Vercel Edge |
| AWS Lambda | Yes | Through permdock/node or a Fetch adapter such as Hono's; approvals need an external ApprovalStore |
| Browsers, React Native (Hermes) | Client entries only | permdock/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 arktypePermDock 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 path | Contents |
|---|---|
permdock | definePermissions, resource, mergePermissions, listPermissions, findPermission, definePolicy, defineRoles, definePlans, role, allow, deny, principal, context, createPermDock, errors |
permdock/react | PermDockProvider, usePermDock, usePermission, <Protected> |
permdock/next | createPermDock returning getPermDock, getPermission, PermDockProvider, permdockHandler |
permdock/next/plugin | createPermDockPlugin (build-time collect hook only) |
permdock/unplugin | createPermDockUnplugin (the same collect hook for Vite, Rollup, webpack, Rspack and esbuild; recipes for TanStack Start, React Router, Nuxt, Astro, Effect) |
permdock/server | Fetch-first kernel shared by the HTTP adapters |
permdock/hono | createPermDock returning permdock middleware and protect |
permdock/express, permdock/fastify, permdock/elysia, permdock/nest, permdock/node | Same shape as permdock/hono |
permdock/jwt | subjectFromJwt, createJwtSubjectResolver: JWKS verification and claim mapping to a subject (jose optional peer) |
permdock/terminal | createPermDock returning permdock, protect, filterCommands, format for your own CLI |
permdock/trpc, permdock/orpc | RPC middleware with OpenAPI hooks |
permdock/mcp | createPermDock returning protectServer |
permdock/ai-sdk | createPermDock returning toolApproval, capabilityMiddleware, needsApproval |
permdock/claude-agent | createPermDock returning canUseTool, permissionRequestHook |
permdock/eve | createPermDock returning approval, approvalFor, permdock for Eve defineTool |
permdock/openai | createPermDock returning needsApproval, guardTools, resolveInterruptions, permdock for the OpenAI Agents SDK |
permdock/approvals | ApprovalStore interface, memoryApprovalStore, approvalsHandler; the store option of every agent and HTTP adapter |
permdock/cloud | cloud({ url, key }) returning approvals, sink, snapshots for PermDock Cloud; optional, server-only, never on the decision path |
permdock/webmcp | registerTools |
permdock/a2a | createPermDock returning agentCard, extendedAgentCard |
permdock/authzen | createPermDock returning the AuthZEN permdockHandler |
permdock/ssf | createPermDock returning the CAEP receiver |
permdock/scim | scimHandler, the DirectoryStore interface, memoryDirectoryStore, directoryMembershipSource |
permdock/openapi | OpenAPI emission (document or Overlay) and import |
permdock/otel | Span per check and a decision counter |
permdock/react-native | React adapter plus persisted storage |
permdock/vue, permdock/svelte, permdock/solid | UI adapters |
permdock/drizzle, permdock/prisma, permdock/kysely | toWhere condition compilers |
permdock/supabase, permdock/better-auth, permdock/clerk, permdock/convex, permdock/pdp | Subject 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:
| Entry | Optional peer | Why |
|---|---|---|
permdock/jwt | jose | JWS verification and JWKS fetching; see JWT |
permdock/otel | @opentelemetry/api | Span and metric emission |
permdock/supabase, permdock/clerk, permdock/better-auth, permdock/convex | The provider SDK, at the call site | The 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/terminal | An OS keychain binding | Token storage; falls back to a mode-0600 file |
permdock/testing | vitest | describePolicy and the runners register Vitest tests |
permdock rls import | pgsql-parser | Parses 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 --db | pg | Talks to a live database |
permdock/unplugin | unplugin | The 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/otelbehind a type-onlyTracerinterface 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 whatcollect,doctorand the build hooks need;yamlandajvare bundled into the lazily loaded OpenAPI command chunks, and the Postgres parser,pg,unpluginand 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
| File | Contents | Imported by |
|---|---|---|
src/permissions.ts | definePermissions() or mergePermissions(); no rules | Everything, including client bundles |
src/policy.ts | definePolicy() with roles | Server only |
src/permdock/server.ts | createPermDock from a server adapter | Server code |
src/permissions.generated.ts | Output of permdock rls import or openapi import | src/permissions.ts |
permissions.catalog.json | Output of permdock collect; committed | CI (--check), docs, OpenAPI |
The quick start fills in the first three.
Verify the install
pnpm permdock doctordoctor 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