---
title: Merging Policies
description: Combine multiple policies with AND or OR strategies in permit.
type: package
package: "@zap-studio/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

:::note

Merged policies evaluate in parallel and never short-circuit: every policy is invoked regardless of outcome. `mergePoliciesAnd()` requires all results to allow, `mergePoliciesOr()` requires at least one — but both wait for every policy's `can()` to settle first.

:::

## mergePoliciesAnd() — AND Strategy

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

```ts
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

```ts
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.

```ts
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

```ts
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:

```ts
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

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

## Empty Policy Lists

Merging zero policies always denies, with either strategy:

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

## Best Practices

1. **Name policies descriptively** — `securityPolicy`, `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
