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

Merging Policies

Combine multiple policies with AND or OR strategies in permit.

permit merges policies via mergePoliciesAnd and mergePoliciesOr. As your application grows, you may want to organize authorization logic into separate policies. permit provides two merge strategies: AND and OR.

Why Merge Policies?

Merging policies is useful for:

  • Separation of concerns — Keep domain-specific rules in separate files
  • Layered security — Apply base rules that can be tightened or relaxed
  • Feature flags — Enable/disable features by adding or removing policies
  • Multi-tenancy — Combine organization policies with application policies

mergePoliciesAnd() — AND Strategy

The mergePoliciesAnd() function combines policies using an AND strategy. An action is allowed only if all policies allow it.

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

const mergedPolicy = mergePoliciesAnd(policy1, policy2, policy3);

Think of it as an AND operation: policy1 AND policy2 AND policy3

Behavior

Policy 1 Policy 2 Result
allow allow allow
allow deny deny
deny allow deny
deny deny deny

Example: Base + Restrictive Policy

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

const resources = {
  document: z.object({
    id: z.string(),
    ownerId: z.string(),
    classification: z.enum(["public", "internal", "confidential"]),
  }),
} satisfies Resources;

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

type AppContext = {
  user: { id: string; clearanceLevel: number } | null;
};

// Base policy: standard access rules
const basePolicy = createPolicy<AppContext>({
  resources,
  actions,
  rules: {
    document: {
      read: when((ctx, _, doc) => doc.classification === "public" || ctx.user?.id === doc.ownerId),
      write: when((ctx, _, doc) => ctx.user?.id === doc.ownerId),
      delete: when((ctx, _, doc) => ctx.user?.id === doc.ownerId),
    },
  },
});

// Security policy: additional clearance requirements
const securityPolicy = createPolicy<AppContext>({
  resources,
  actions,
  rules: {
    document: {
      read: when((ctx, _, doc) => {
        if (doc.classification === "confidential") {
          return (ctx.user?.clearanceLevel ?? 0) >= 3;
        }
        if (doc.classification === "internal") {
          return (ctx.user?.clearanceLevel ?? 0) >= 1;
        }
        return true;
      }),
      write: when((ctx, _, doc) => {
        if (doc.classification === "confidential") {
          return (ctx.user?.clearanceLevel ?? 0) >= 3;
        }
        return true;
      }),
      delete: when((ctx, _, doc) => {
        if (doc.classification === "confidential") {
          return (ctx.user?.clearanceLevel ?? 0) >= 4;
        }
        return true;
      }),
    },
  },
});

// Merge: both policies must allow
const policy = mergePoliciesAnd(basePolicy, securityPolicy);

// Usage
const confidentialDoc = {
  id: "doc-1",
  ownerId: "user-123",
  classification: "confidential" as const,
};

// Owner with low clearance: base allows, security denies → DENIED
const lowClearanceOwner: AppContext = {
  user: { id: "user-123", clearanceLevel: 1 },
};
console.log(await policy.can(lowClearanceOwner, "document:read", confidentialDoc)); // false

// Owner with high clearance: both allow → ALLOWED
const highClearanceOwner: AppContext = {
  user: { id: "user-123", clearanceLevel: 3 },
};
console.log(await policy.can(highClearanceOwner, "document:read", confidentialDoc)); // true

mergePoliciesOr() — OR Strategy

The mergePoliciesOr() function combines policies using an OR strategy. An action is allowed if any policy allows it.

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

const mergedPolicy = mergePoliciesOr(policy1, policy2, policy3);

Think of it as an OR operation: policy1 OR policy2 OR policy3

Behavior

Policy 1 Policy 2 Result
allow allow allow
allow deny allow
deny allow allow
deny deny deny

Example: Multiple Access Paths

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

const resources = {
  file: z.object({
    id: z.string(),
    ownerId: z.string(),
    isPublic: z.boolean(),
    sharedWith: z.array(z.string()),
    teamId: z.string().nullable(),
  }),
} satisfies Resources;

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

type FileContext = {
  user: { id: string; teamIds: string[] } | null;
};

// Owner access policy
const ownerPolicy = createPolicy<FileContext>({
  resources,
  actions,
  rules: {
    file: {
      read: when((ctx, _, file) => ctx.user?.id === file.ownerId),
      write: when((ctx, _, file) => ctx.user?.id === file.ownerId),
      delete: when((ctx, _, file) => ctx.user?.id === file.ownerId),
    },
  },
});

// Public access policy
const publicPolicy = createPolicy<FileContext>({
  resources,
  actions,
  rules: {
    file: {
      read: when((_, __, file) => file.isPublic),
      write: when(() => false),
      delete: when(() => false),
    },
  },
});

// Shared access policy
const sharedPolicy = createPolicy<FileContext>({
  resources,
  actions,
  rules: {
    file: {
      read: when((ctx, _, file) => file.sharedWith.includes(ctx.user?.id ?? "")),
      write: when((ctx, _, file) => file.sharedWith.includes(ctx.user?.id ?? "")),
      delete: when(() => false),
    },
  },
});

// Team access policy
const teamPolicy = createPolicy<FileContext>({
  resources,
  actions,
  rules: {
    file: {
      read: when(
        (ctx, _, file) =>
          file.teamId !== null && (ctx.user?.teamIds.includes(file.teamId) ?? false),
      ),
      write: when(
        (ctx, _, file) =>
          file.teamId !== null && (ctx.user?.teamIds.includes(file.teamId) ?? false),
      ),
      delete: when(() => false),
    },
  },
});

// Any of these policies can grant access
const filePolicy = mergePoliciesOr(ownerPolicy, publicPolicy, sharedPolicy, teamPolicy);

// Usage
const file = {
  id: "file-1",
  ownerId: "user-owner",
  isPublic: false,
  sharedWith: ["user-shared"],
  teamId: "team-1",
};

const sharedUser: FileContext = {
  user: { id: "user-shared", teamIds: [] },
};
console.log(await filePolicy.can(sharedUser, "file:read", file)); // true (via sharedPolicy)
console.log(await filePolicy.can(sharedUser, "file:write", file)); // true (via sharedPolicy)
console.log(await filePolicy.can(sharedUser, "file:delete", file)); // false (no policy allows)

Combining Both Strategies

You can combine mergePoliciesAnd() and mergePoliciesOr() for complex scenarios:

import { mergePoliciesAnd, mergePoliciesOr } from "@zap-studio/permit";

// Access layer: multiple paths to access
const accessPolicy = mergePoliciesOr(ownerPolicy, sharedPolicy, publicPolicy);

// Security layer: must pass all security checks
const securedPolicy = mergePoliciesAnd(auditPolicy, compliancePolicy, ratePolicy);

// Final policy: must have access AND pass security
const finalPolicy = mergePoliciesAnd(accessPolicy, securedPolicy);

Visualization

                   ┌─────────────┐
                   │ finalPolicy │
                   └──────┬──────┘
                          │ AND (mergePoliciesAnd)
            ┌─────────────┴─────────────┐
            │                           │
     ┌──────┴──────┐             ┌──────┴───────┐
     │accessPolicy │             │securedPolicy │
     └──────┬──────┘             └──────┬───────┘
            │ OR                        │ AND
    ┌───────┼───────┐           ┌───────┼───────┐
    │       │       │           │       │       │
 owner   shared   public     audit  compliance rate

Empty Policy Lists

Merging zero policies always denies, with either strategy:

const emptyAnd = mergePoliciesAnd(); // can() always resolves to false
const emptyOr = mergePoliciesOr(); // can() always resolves to false

Best Practices

  1. Name policies descriptivelysecurityPolicy, ownerAccessPolicy, compliancePolicy
  2. Keep policies focused — Each policy should handle one concern
  3. Document why each policy is included — Argument order has no effect on the result; explain each policy’s role instead of relying on its position
  4. Test merged policies — Ensure the combined behavior is correct
  5. Keep merged policies cheap — Every policy runs on every check, even after another has already decided the outcome; avoid expensive work (I/O, heavy computation) inside a merged policy’s rules

Last updated on September 15, 2026

Was this page helpful?