Getting Started
Define resources, actions, context, and rules to build your first authorization policy with permit.
A policy is the core building block of permit. It defines what actions users can perform on your application’s resources based on the current context.
Installation
npm install @zap-studio/permityarn add @zap-studio/permitpnpm add @zap-studio/permitbun add @zap-studio/permitdeno add jsr:@zap-studio/permitYou also need a schema library that implements Standard Schema, such as Zod, Valibot, or ArkType.
Understanding Policies
Think of a policy as a rulebook that answers questions like:
- “Can this user edit this blog post?”
- “Can a guest view this private document?”
- “Can an admin delete any comment?”
A policy consists of three parts:
- Resources — The things being protected (posts, comments, users)
- Actions — What can be done with resources (read, write, delete)
- Rules — The logic that determines if an action is allowed
Defining Resources
Resources are defined using Standard Schema, which means you can use Zod, Valibot, ArkType, or any compatible library.
import { z } from "zod";
import type { Resources } from "@zap-studio/permit";
const resources = {
post: z.object({
id: z.string(),
authorId: z.string(),
title: z.string(),
visibility: z.enum(["public", "private", "draft"]),
createdAt: z.date(),
}),
comment: z.object({
id: z.string(),
postId: z.string(),
authorId: z.string(),
content: z.string(),
}),
user: z.object({
id: z.string(),
email: z.email(),
role: z.enum(["guest", "user", "admin"]),
}),
} satisfies Resources;
See Standard Schema Support for the same definition with Valibot and ArkType.
Defining Actions
Actions specify what operations are allowed on each resource. Define them as readonly arrays:
import type { Actions } from "@zap-studio/permit";
const actions = {
post: ["read", "write", "delete", "publish"],
comment: ["read", "write", "delete"],
user: ["read", "update", "delete", "ban"],
} as const satisfies Actions<typeof resources>;
Understanding Context
Context represents the runtime information available when checking permissions. This typically includes the current user, but can contain anything relevant to authorization:
type AppContext = {
user: {
id: string;
role: "guest" | "user" | "admin";
permissions: string[];
organizationId: string;
} | null;
request?: {
ip: string;
userAgent: string;
};
timestamp: Date;
};
Context is passed to policy.can() at runtime and is available in all your rule functions.
Creating a Policy
Use createPolicy() to combine your resources, actions, and rules into a policy. See Declarative Policies for the full reference on allow(), deny(), and when().
import { createPolicy, allow, deny, when } from "@zap-studio/permit";
const policy = createPolicy<AppContext>({
resources,
actions,
rules: {
post: {
read: allow(),
write: when((ctx, _action, post) => ctx.user?.id === post.authorId),
delete: deny(),
publish: when((ctx) => ctx.user?.role === "admin"),
},
comment: {
read: allow(),
write: when((ctx) => ctx.user !== null),
delete: when((ctx, _action, comment) => ctx.user?.id === comment.authorId),
},
},
});
Checking Permissions
The policy object provides a can() method. It takes the context, a "resource:action" permission string, and the resource, then returns a Promise<boolean>:
policy.can(context, permission, resource): Promise<boolean>
Parameters
| Parameter | Type | Description |
|---|---|---|
context |
TContext |
The current context (user, request, etc.) |
permission |
string |
The permission to check (e.g., "post:read", "comment:write") |
resource |
object |
The actual resource being accessed |
How can() Decides
can() never throws — it resolves to false whenever a check fails:
- Parse the permission — Malformed strings (missing or extra
:segments) returnfalse. - Check the action — Actions not listed for the resource in
actionsreturnfalse. - Validate the resource — The resource is validated against its Standard Schema; invalid resources return
false. - Evaluate the rule — No rule defined for the action returns
false. If the rule throws, the error is logged withconsole.warnandcan()returnsfalse.
Real-World Example: E-commerce Store
Here’s a complete example for an e-commerce application:
import { z } from "zod";
import { createPolicy, allow, when, or } from "@zap-studio/permit";
import type { Resources, Actions } from "@zap-studio/permit";
// Define resources
const resources = {
product: z.object({
id: z.string(),
sellerId: z.string(),
price: z.number(),
status: z.enum(["draft", "published", "archived"]),
}),
order: z.object({
id: z.string(),
customerId: z.string(),
sellerId: z.string(),
status: z.enum(["pending", "paid", "shipped", "delivered"]),
}),
review: z.object({
id: z.string(),
productId: z.string(),
customerId: z.string(),
rating: z.number().min(1).max(5),
}),
} satisfies Resources;
const actions = {
product: ["read", "create", "update", "delete", "publish"],
order: ["read", "create", "update", "cancel"],
review: ["read", "create", "update", "delete"],
} as const satisfies Actions<typeof resources>;
type StoreContext = {
user: {
id: string;
role: "customer" | "seller" | "admin";
} | null;
};
const storePolicy = createPolicy<StoreContext>({
resources,
actions,
rules: {
product: {
// Anyone can read published products
read: when((_, __, product) => product.status === "published"),
// Only sellers can create products
create: when((ctx) => ctx.user?.role === "seller"),
// Sellers can update their own products
update: when(
(ctx, _, product) => ctx.user?.role === "seller" && ctx.user.id === product.sellerId,
),
// Only admins can delete products
delete: when((ctx) => ctx.user?.role === "admin"),
// Sellers can publish their own products
publish: when(
(ctx, _, product) => ctx.user?.role === "seller" && ctx.user.id === product.sellerId,
),
},
order: {
// Customers see their orders, sellers see orders for their products
read: when(
or(
(ctx, _, order) => ctx.user?.id === order.customerId,
(ctx, _, order) => ctx.user?.id === order.sellerId,
),
),
// Only authenticated customers can create orders
create: when((ctx) => ctx.user?.role === "customer"),
// Sellers can update order status
update: when((ctx, _, order) => ctx.user?.id === order.sellerId),
// Customers can cancel pending orders
cancel: when(
(ctx, _, order) => ctx.user?.id === order.customerId && order.status === "pending",
),
},
review: {
// Anyone can read reviews
read: allow(),
// Customers can create reviews
create: when((ctx) => ctx.user?.role === "customer"),
// Customers can update their own reviews
update: when((ctx, _, review) => ctx.user?.id === review.customerId),
// Customers can delete their reviews, admins can delete any
delete: when(
or(
(ctx, _, review) => ctx.user?.id === review.customerId,
(ctx) => ctx.user?.role === "admin",
),
),
},
},
});
// Usage
const product = {
id: "prod-1",
sellerId: "seller-123",
price: 99.99,
status: "published" as const,
};
const customerContext: StoreContext = {
user: { id: "customer-456", role: "customer" },
};
console.log(await storePolicy.can(customerContext, "product:read", product)); // true
console.log(await storePolicy.can(customerContext, "product:update", product)); // false
Best Practices
- Keep resources focused — Each resource should represent a single domain entity
- Use descriptive action names — “publish” is clearer than “update-status”
- Include all relevant data in context — Don’t fetch additional data inside rules
- Be explicit about what’s allowed — Actions without a rule are denied by default
- Test your policies — Write unit tests for critical authorization rules