permit
A type-safe, declarative authorization library for TypeScript with Standard Schema validation and composable conditions.
permit is a type-safe, declarative authorization library for TypeScript with Standard Schema support.
Motivation
Authorization checks written by hand, like if (user.role === "admin"), spread through a codebase over time. After a while, nobody can answer “who is allowed to delete a post?” without searching the whole app.
A framework like CASL solves the spreading problem, but it comes with its own vocabulary to learn (subject, can, cannot, rules), and its rules are not checked against your actual data shapes — you can write a rule that references a field your resource does not have, and it will only fail once that code runs.
permit keeps all rules in one place, through createPolicy(...) with allow(), deny(), and when(condition) — one file answers “who can do what.”
And because resources come from your Standard Schema schemas, policy types are derived straight from your real data shapes. Reference a field that does not exist, and you get an error while writing the code, not a silent undefined in production.
Features
- Full type safety — actions, resources, and permissions are inferred from your schemas and
satisfiesdeclarations, from Getting Started onward. - Standard Schema support via
Resources— works with Zod, Valibot, ArkType, or any compatible library. See Standard Schema Support. - Declarative policies through
createPolicy(...)withallow(),deny(), andwhen(condition). See Declarative Policies. - Role hierarchy support via
hasRole(role, hierarchy?), with inheritance resolved bycollectInheritedRoles. See Role-Based Access Control. - Composable conditions via
and,or, andnot. See Conditions. - Policy merging strategies via
mergePoliciesAndandmergePoliciesOr. See Merging Policies. - Structured errors with
PolicyErrorfor invalid configuration or evaluation failures. See Error Handling. - Optional logging through
createPolicy({ logger })fromlogger— omit it and there’s zero added logging overhead. - Native OpenTelemetry — an
INTERNALspan per check with the allow/deny decision as an attribute, plus apermit.checkscounter, no-op until an SDK is registered. - Tree-shakeable — policies and conditions are plain functions; unused exports are dropped by any modern bundler.
Quick Start
import { z } from "zod";
import { ConsoleLogger } from "@zap-studio/logger";
import { createPolicy, allow, deny, when } from "@zap-studio/permit";
import type { Resources, Actions } from "@zap-studio/permit";
const resources = {
post: z.object({ id: z.string(), authorId: z.string() }),
} satisfies Resources;
const actions = {
post: ["read", "write", "delete"],
} as const satisfies Actions<typeof resources>;
type AppContext = { user: { id: string } };
const logger = new ConsoleLogger({ minLevel: "debug" });
const policy = createPolicy<AppContext>({
resources,
actions,
rules: {
post: {
read: allow(),
write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
delete: deny(),
},
},
logger,
});
const ctx: AppContext = { user: { id: "user-1" } };
const post = { id: "1", authorId: "user-1" };
await policy.can(ctx, "post:write", post); // true, inferred as boolean
Continue with Getting Started for a full walkthrough of resources, actions, context, and checking permissions.
Runtime Support
| Runtime | Minimum version |
|---|---|
| Node.js | 18.0.0 |
| Bun | 1.0.0 |
| Deno | 1.42 |
| Cloudflare Workers | Any current release |
| Browsers | Latest evergreen (Chrome, Edge, Firefox, Safari) |
The package ships standard ESM only and uses no runtime-specific APIs. Deno 1.42 is the first release that can install packages from JSR (deno add jsr:@zap-studio/permit).