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

Declarative Policies

Through createPolicy(...) with allow(), deny(), and when(condition) — define authorization rules in permit.

createPolicy(...) builds policies using allow(), deny(), and when(condition). permit provides three rule builders for defining what’s allowed.

Understanding Rules

Every rule returns a decision: either "allow" or "deny". Rules receive three arguments:

  1. context — The current user/request context
  2. action — The action being performed (e.g., "read", "write")
  3. resource — The resource being accessed (already validated against its schema)

Setup

The examples on this page use these definitions:

import { z } from "zod";
import type { Resources, Actions } from "@zap-studio/permit";

const resources = {
  post: z.object({
    id: z.string(),
    authorId: z.string(),
    visibility: z.enum(["public", "private"]),
  }),
} satisfies Resources;

const actions = {
  post: ["read", "write", "delete"],
} as const satisfies Actions<typeof resources>;

type AppContext = {
  user: { id: string; role: "guest" | "user" | "admin" } | null;
};

allow()

The allow() function creates a rule that always permits the action, regardless of context or resource.

import { allow } from "@zap-studio/permit";

When to Use

Use allow() for actions that should be available to everyone:

  • Public content (published blog posts, product listings)
  • Health check endpoints
  • Public API documentation

Example: Public Content

import { createPolicy, allow } from "@zap-studio/permit";

const policy = createPolicy<AppContext>({
  resources,
  actions,
  rules: {
    post: {
      // Anyone can read posts (refine this with conditions later)
      read: allow(),
    },
  },
});

deny()

The deny() function creates a rule that always blocks the action. No context or resource can override this.

import { deny } from "@zap-studio/permit";

When to Use

Use deny() for:

  • Temporarily disabled features
  • Actions reserved for future implementation
  • Hard blocks that should never be bypassed

Example: Disabled Actions

import { createPolicy, allow, deny, when } from "@zap-studio/permit";

const policy = createPolicy<AppContext>({
  resources,
  actions,
  rules: {
    post: {
      read: allow(),
      write: when((ctx, _, post) => ctx.user?.id === post.authorId),
      // Deleting posts is disabled for everyone
      delete: deny(),
    },
  },
});

when()

The when() function creates a conditional rule. It takes a condition function and allows the action only if the condition returns true.

import { when } from "@zap-studio/permit";

when((context, action, resource) => boolean);

Condition Function

The condition function receives:

Parameter Type Description
context TContext Current user/request context
action TAction The action being performed
resource TResource The resource being accessed

It must return a boolean:

  • true → action is allowed
  • false → action is denied

Example: Owner-Only Access

import { createPolicy, allow, when } from "@zap-studio/permit";

const policy = createPolicy<AppContext>({
  resources,
  actions,
  rules: {
    post: {
      read: allow(),

      // Only the author can edit their post
      write: when((ctx, _, post) => ctx.user?.id === post.authorId),

      // Only the author can delete their post
      delete: when((ctx, _, post) => ctx.user?.id === post.authorId),
    },
  },
});

Using the Action Parameter

The action parameter becomes useful when you create reusable condition functions that handle multiple actions differently. This lets you share logic across actions while still customizing behavior:

import { createPolicy, when } from "@zap-studio/permit";
import type { ConditionFn } from "@zap-studio/permit";

type PostAction = "read" | "write" | "delete";
type Post = { id: string; authorId: string; visibility: "public" | "private" };

// Reusable condition that behaves differently based on action
const canAccessPost: ConditionFn<AppContext, PostAction, Post> = (ctx, action, post) => {
  // Anyone can read public posts
  if (action === "read" && post.visibility === "public") {
    return true;
  }

  // For write/delete (or private reads), must be the author
  return ctx.user?.id === post.authorId;
};

const policy = createPolicy<AppContext>({
  resources,
  actions,
  rules: {
    post: {
      // Same function handles all three actions with different logic
      read: when(canAccessPost),
      write: when(canAccessPost),
      delete: when(canAccessPost),
    },
  },
});

This pattern is useful when actions share similar logic but need slight variations — you define the condition once and reuse it across multiple actions.

Combining Rules

Conditions can be composed with combinators. See Conditions for details on and(), or(), not(), and has().

Best Practices

  1. Start restrictive — Leave actions without a rule (or use deny()), then explicitly allow
  2. Keep conditions pure — Don’t perform side effects in condition functions
  3. Avoid async operations — Conditions must be synchronous; fetch data before checking
  4. Use descriptive variable namesisOwner, isMember, hasAccess
  5. Extract complex conditions — Create reusable condition functions for clarity

Last updated on September 21, 2026

Was this page helpful?