Skip to content
Zap Studio
permit
Esc
navigateopen⌘Jpreview
On this page

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/permit
yarn add @zap-studio/permit
pnpm add @zap-studio/permit
bun add @zap-studio/permit
deno add jsr:@zap-studio/permit

You 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:

  1. Resources — The things being protected (posts, comments, users)
  2. Actions — What can be done with resources (read, write, delete)
  3. 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:

  1. Parse the permission — Malformed strings (missing or extra : segments) return false.
  2. Check the action — Actions not listed for the resource in actions return false.
  3. Validate the resource — The resource is validated against its Standard Schema; invalid resources return false.
  4. Evaluate the rule — No rule defined for the action returns false. If the rule throws, the error is logged with console.warn and can() returns false.

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

  1. Keep resources focused — Each resource should represent a single domain entity
  2. Use descriptive action names — “publish” is clearer than “update-status”
  3. Include all relevant data in context — Don’t fetch additional data inside rules
  4. Be explicit about what’s allowed — Actions without a rule are denied by default
  5. Test your policies — Write unit tests for critical authorization rules

Last updated on September 21, 2026

Was this page helpful?