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:
- context — The current user/request context
- action — The action being performed (e.g.,
"read","write") - 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 allowedfalse→ 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
- Start restrictive — Leave actions without a rule (or use
deny()), then explicitly allow - Keep conditions pure — Don’t perform side effects in condition functions
- Avoid async operations — Conditions must be synchronous; fetch data before checking
- Use descriptive variable names —
isOwner,isMember,hasAccess - Extract complex conditions — Create reusable condition functions for clarity