npm package: @gianstack/next-clerk-pbac
Description: A publishable Next.js App Router + Clerk package for PBAC capability catalogs, actor resolution, and synchronous authorization policies.
Latest version: 0.2.0

# `@gianstack/next-clerk-pbac`

## Introduction

### What this package is

`@gianstack/next-clerk-pbac` is an opinionated PBAC package for **Next.js App Router + Clerk**.

It exists to make one authorization flow explicit and repeatable:

1. define a typed capability catalog;
2. declare the Clerk roles defined for your app;
3. map the subset of those roles that grants base capabilities;
4. create one shared PBAC config;
5. compose shared synchronous policies from that config;
6. resolve the current actor through a server or client boundary adapter;
7. evaluate a policy and branch on a decision union that is explicit about denial and access scope.

### What it gives you

- a typed capability catalog that produces stable `resource.action` strings;
- a shared PBAC config that centralizes the capability catalog, the Clerk roles declared for your app, the base role mapping, and effective capability resolution;
- a discriminated `Actor` model that rules out impossible states;
- small policy helpers that stay synchronous and pure;
- an authorization decision union that makes denial and access scope explicit;
- boundary-specific adapters for server and client actor resolution.
- a user-defined access model, so policies can return whatever success payload your app actually needs.

### What it does not do

This package does **not** handle:

- transport error mapping for HTTP, GraphQL, or tRPC;
- ORM integration or database access;
- automatic query generation;
- async policies;
- Pages Router support;
- standalone workers, scripts, or cron runtimes;
- per-user capability storage inside Clerk or JWT claims.

### Policy-first mental model

- Clerk remains the source of truth for identity, session, active organization, and active organization role.
- Your application defines the capability catalog, mirrors the Clerk roles defined for your app in `roles`, defines the base role mapping, and composes shared policies.
- Policies stay synchronous and pure. If a decision needs database facts, load them before the policy or enforce them in the query layer.
- Application code should usually evaluate shared policies, not inspect raw capabilities directly.
- A successful decision should tell the next layer **how** it is allowed to proceed, not just whether it is allowed.
- The package does not impose a built-in access taxonomy. Your app defines the success payload that policies return.

### Support matrix

- Next.js `16.x` with the App Router
- Clerk Next.js `7.x`
- React `19.x`
- Node.js `>=20.9.0`
- ESM only

### Install

```bash
pnpm add @gianstack/next-clerk-pbac @clerk/nextjs
```

Prerequisites:

- a Next.js App Router application;
- Clerk middleware enabled in the consumer app;
- React `19.x`;
- Node.js `>=20.9.0`.

### Recommended import contract

- import shared config, policy helpers, and types from `@gianstack/next-clerk-pbac`;
- import the client adapter factory from `@gianstack/next-clerk-pbac/client`;
- import the server adapter factory from `@gianstack/next-clerk-pbac/server`;
- treat a local hook built on `pbacClient.useActorState()` plus `ActorState` as the primary client contract;
- treat `access` as the explicit success scope returned by a policy;
- treat `AccessOfPolicy` and `ReasonOfPolicy` as the official derived helper types.

### User-defined access model

This package does not export a built-in access taxonomy.

You define the success payload that your policies return. A simple app may use:

```ts
type PostAccess = "any" | "own" | "sameOrganization";
```

An app with richer authorization semantics may use objects instead:

```ts
type MembershipAccess =
  | "any"
  | { kind: "organizations"; organizationIds: readonly string[] };
```

Both shapes are valid. The package only propagates `TAccess`; it does not constrain it.

## Step-by-Step Implementation

### Step 1 Define the capability catalog

Create the catalog once. This is the static list of capabilities that can exist in the application.

```ts
import { defineCapabilities } from "@gianstack/next-clerk-pbac";

// Define the resource/action matrix once for the whole app.
const capabilities = defineCapabilities({
  dashboard: ["access"],
  post: ["readAny", "readOwn", "updateAny", "updateOwn"],
} as const);
```

Relevant APIs: [defineCapabilities](#definecapabilities), [CapabilityDefinition](#capabilitydefinition), [CapabilityCatalog](#capabilitycatalog)

### Step 2 Declare the Clerk roles defined for your app and map base capabilities

Declare every Clerk organization role defined for your app, then map only the subset that should grant base capabilities. `roles` and `roleCapabilities` are not the same thing.

```ts
import type {
  CapabilityOfCatalog,
  RoleCapabilitiesMap,
} from "@gianstack/next-clerk-pbac";

// Declare every Clerk organization role defined for the app.
const roles = ["org:admin", "org:member"] as const;
type AppRole = (typeof roles)[number];
type AppCapability = CapabilityOfCatalog<typeof capabilities>;

// Constrain the mapping right where you declare it:
// - keys must come from `roles`
// - values must come from the capability catalog
const roleCapabilities = {
  "org:admin": [
    capabilities.dashboard.access,
    capabilities.post.readAny,
    capabilities.post.updateAny,
  ],
  "org:member": [
    capabilities.post.readOwn,
    capabilities.post.updateOwn,
  ],
} as const satisfies RoleCapabilitiesMap<AppRole, AppCapability>;
```

Relevant APIs: [createClerkPbac](#createclerkpbac), [CapabilityOfCatalog](#capabilityofcatalog), [RoleCapabilitiesMap](#rolecapabilitiesmap)

### Step 3 Create the shared PBAC config

Create a single shared `pbac` object. This is the config both boundary adapters and policies will consume.

```ts
import {
  type ActorOfConfig,
  type ActorState,
  type CapabilityOfCatalog,
  type RoleCapabilitiesMap,
  createClerkPbac,
  defineCapabilities,
} from "@gianstack/next-clerk-pbac";

const capabilities = defineCapabilities({
  dashboard: ["access"],
  post: ["readAny", "readOwn", "updateAny", "updateOwn"],
} as const);

const roles = ["org:admin", "org:member"] as const;
type AppRole = (typeof roles)[number];
type AppCapability = CapabilityOfCatalog<typeof capabilities>;

const roleCapabilities = {
  "org:admin": [
    capabilities.dashboard.access,
    capabilities.post.readAny,
    capabilities.post.updateAny,
  ],
  "org:member": [
    capabilities.post.readOwn,
    capabilities.post.updateOwn,
  ],
} as const satisfies RoleCapabilitiesMap<AppRole, AppCapability>;

// Reuse the same config across server and client boundaries.
export const pbac = createClerkPbac({
  capabilities,
  roles,
  roleCapabilities,
});

// Derive app-level convenience types once, next to the shared config.
export type AppActor = ActorOfConfig<typeof pbac>;
export type AppActorState = ActorState<AppActor>;
```

Relevant APIs: [createClerkPbac](#createclerkpbac), [ClerkPbacConfig](#clerkpbacconfig)

### Step 4 Define shared policies

Policies are the part that the rest of the application should actually consume. Use `pbac.capabilities` as the default source of capabilities.

```ts
import {
  allowIfAll,
  allowIfAny,
  createPolicy,
  hasCapability,
  isAuthenticated,
} from "@gianstack/next-clerk-pbac";

import { pbac } from "./pbac";

// Build the "any" allowed branch first: admins can update any post.
const allowUpdateOnAnyPost = allowIfAll(
  { access: "any" },
  isAuthenticated({
    code: "NOT_AUTHENTICATED",
    message: "You must be authenticated.",
  }),
  hasCapability(pbac.capabilities.post.updateAny, {
    code: "MISSING_UPDATE_ANY",
    message: "Missing capability post.updateAny.",
  }),
);

// Build the "own" allowed branch second: members can update only their own posts.
const allowUpdateOnOwnedPosts = allowIfAll(
  { access: "own" },
  isAuthenticated({
    code: "NOT_AUTHENTICATED",
    message: "You must be authenticated.",
  }),
  hasCapability(pbac.capabilities.post.updateOwn, {
    code: "MISSING_UPDATE_OWN",
    message: "Missing capability post.updateOwn.",
  }),
);

// Compose both allowed branches into one shared policy.
// `access` is a consumer-defined success payload returned on an allowed branch.
export const updatePostPolicy = createPolicy(
  allowIfAny(allowUpdateOnAnyPost, allowUpdateOnOwnedPosts),
);
```

Relevant APIs: [createPolicy](#createpolicy), [allowIfAll](#allowifall), [allowIfAny](#allowifany), [isAuthenticated](#isauthenticated), [hasCapability](#hascapability), [Policy](#policy)

### Step 5 Create the server boundary adapter

The server is the real enforcement boundary. Build a server adapter from the shared config and resolve the actor there.

```ts
import { createClerkPbacServer } from "@gianstack/next-clerk-pbac/server";

import { pbac } from "./pbac";

// Create the server adapter once in a server-only module.
export const pbacServer = createClerkPbacServer(pbac);

// Later, inside a Server Component, route handler, or server procedure:
// const actor = await pbacServer.getActor();
```

Relevant APIs: [createClerkPbacServer](#createclerkpbacserver), [Actor](#actor), [ActorOfConfig](#actorofconfig)

### Step 6 Create the client boundary adapter

The client gets a UI-facing snapshot of the actor. Create the package adapter once in a dedicated client module, then wrap it in an app-level hook that your components can consume directly.

```tsx
// pbac-client.ts
"use client";

import { createClerkPbacClient } from "@gianstack/next-clerk-pbac/client";

import { pbac, type AppActorState } from "./pbac";

const pbacClient = createClerkPbacClient(pbac);

// Expose one app-level hook with the shared AppActorState alias.
export function useAppActorState(): AppActorState {
  return pbacClient.useActorState();
}
```

```tsx
// dashboard-gate.tsx
"use client";

import { useAppActorState } from "./pbac-client";

export function DashboardGate() {
  const { isPending, actor } = useAppActorState();

  if (isPending) {
    return null;
  }

  return <span>{String(actor.authenticated)}</span>;
}
```

Relevant APIs: [createClerkPbacClient](#createclerkpbacclient), [ActorState](#actorstate), [ActorOfConfig](#actorofconfig)

### Step 7 Evaluate a policy with a typed actor

Use the actor type derived from the shared config instead of hand-writing the actor shape.

```ts
import {
  authorize,
} from "@gianstack/next-clerk-pbac";

import type { AppActor } from "./pbac";
import { updatePostPolicy } from "./policies";

export function handlePostUpdate(actor: AppActor) {
  // The policy decides whether the operation is denied or returns a user-defined access payload.
  return authorize(updatePostPolicy, { actor });
}
```

Relevant APIs: [authorize](#authorize), [ActorOfConfig](#actorofconfig), [AuthorizationDecision](#authorizationdecision)

### Step 8 Branch on the decision union

Branch on `decision.kind`. When the decision is allowed, use the user-defined `decision.access` payload.

```ts
import {
  authorize,
} from "@gianstack/next-clerk-pbac";

import type { AppActor } from "./pbac";
import { updatePostPolicy } from "./policies";

export function buildQueryScope(actor: AppActor) {
  const decision = authorize(updatePostPolicy, { actor });

  if (decision.kind === "denied") {
    return {
      ok: false,
      reasons: decision.reasons,
    };
  }

  return {
    ok: true,
    scope: decision.access,
  };
}
```

Relevant APIs: [AllowedAuthorizationDecision](#allowedauthorizationdecision), [AuthorizationDecision](#authorizationdecision), [AccessOfPolicy](#accessofpolicy), [ReasonOfPolicy](#reasonofpolicy)

## API Reference

### defineCapabilities

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
function defineCapabilities<const TDefinition extends CapabilityDefinition>(
  definition: TDefinition,
): CapabilityCatalog<TDefinition>;
```

What it is for:

- Turns a static resource/action definition into an immutable typed capability catalog.

When to use it:

- Use it once in your central `pbac.ts` module.

Classification: Primary

Example: [Example defineCapabilities](#example-definecapabilities)

### createClerkPbac

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
function createClerkPbac<
  const TCapabilities extends CapabilityCatalogShape,
  const TKnownRoles extends readonly string[],
>(options: {
  capabilities: TCapabilities;
  roles: TKnownRoles;
  roleCapabilities: RoleCapabilitiesMap<
    Extract<TKnownRoles[number], string>,
    CapabilityOfCatalog<TCapabilities>
  >;
  readonly onUnknownRole?: (event: {
    source: "server" | "client";
    role: string;
    organizationId: string | null;
    userId: string | null;
    roles: readonly Extract<TKnownRoles[number], string>[];
  }) => void;
  resolveCapabilities?: (
    input: ResolveCapabilitiesInput<
      CapabilityOfCatalog<TCapabilities>,
      Extract<TKnownRoles[number], string>
    >,
  ) => readonly CapabilityOfCatalog<TCapabilities>[];
}): ClerkPbacConfig<
  CapabilityOfCatalog<TCapabilities>,
  TKnownRoles,
  TCapabilities
>;
```

What it is for:

- Builds the shared PBAC config that centralizes the capability catalog, the Clerk roles declared for your app, the role-to-capability mapping, and effective capability resolution.
- Normalizes runtime Clerk roles against `roles`, failing closed to `organization.role = null` when Clerk returns an unknown role.

When to use it:

- Use it once per app or auth surface after defining the catalog, the Clerk roles used by your app, and the base role mapping.
- Declare `roleCapabilities` with `satisfies RoleCapabilitiesMap<...>` before the config call so the role and capability constraints are explicit at the declaration site.
- Use `onUnknownRole` only when you want observability for unexpected runtime roles without throwing.

Classification: Primary

Example: [Example createClerkPbac](#example-createclerkpbac)

### allowIf

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
function allowIf<TInput, TReason extends object>(
  predicate: Predicate<TInput>,
  reason: TReason | ((input: TInput) => TReason),
): Guard<TInput, TReason>;
```

What it is for:

- Creates a guard that passes when the predicate is true and returns a reason when it is false.

When to use it:

- Use it for small custom predicates that do not deserve a dedicated helper.

Classification: Advanced

Example: [Example allowIf](#example-allowif)

### allowIfAll

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
function allowIfAll<TInput, TAccess, TReason extends object>(
  options: { access: TAccess },
  ...guards: Guard<TInput, TReason>[]
): Policy<TInput, TAccess, TReason>;
```

What it is for:

- Composes multiple guards into a policy that allows only when all guards pass.

When to use it:

- Use it for the common case where one access path depends on multiple conditions.
- The `access` option is the consumer-defined success payload returned when every guard passes.

Classification: Primary

Example: [Example allowIfAll](#example-allowifall)

### allowIfAny

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
function allowIfAny<const TPolicies extends readonly Policy[]>(
  ...policies: TPolicies
): Policy<
  /* shared input inferred from the supplied policies */,
  /* allowed access union inferred from the supplied policies */,
  /* denied reason union inferred from the supplied policies */
>;
```

What it is for:

- Composes multiple policies and returns the first allowed branch, or the first denial if none allow.

When to use it:

- Use it when a feature has multiple legitimate access paths such as `any` and `own`.
- It preserves the inferred input, access union, and reason union from the supplied policies instead of erasing them to an untyped `Policy`.

Classification: Primary

Example: [Example allowIfAny](#example-allowifany)

### authorize

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
function authorize<TInput, TAccess, TReason extends object>(
  policy: Policy<TInput, TAccess, TReason>,
  input: TInput,
): AuthorizationDecision<TAccess, TReason>;
```

What it is for:

- Evaluates a policy with the input you provide.

When to use it:

- Use it at server or client call sites once you have the actor and any extra policy input.

Classification: Primary

Example: [Example authorize](#example-authorize)

### check

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
function check<TInput>(predicate: Predicate<TInput>): Predicate<TInput>;
```

What it is for:

- Names and reuses a predicate without changing it.

When to use it:

- Use it sparingly when a predicate is clearer as a reusable value than as inline logic.

Classification: Advanced

Example: [Example check](#example-check)

### createPolicy

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
function createPolicy<TInput, TAccess, TReason extends object>(
  rule: Rule<TInput, TAccess, TReason>,
): Policy<TInput, TAccess, TReason>;
```

What it is for:

- Wraps a rule as a named, shareable policy.

When to use it:

- Use it whenever a policy should be exported and reused across the application.

Classification: Primary

Example: [Example createPolicy](#example-createpolicy)

### denyIf

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
function denyIf<TInput, TReason extends object>(
  predicate: Predicate<TInput>,
  reason: TReason | ((input: TInput) => TReason),
): Guard<TInput, TReason>;
```

What it is for:

- Creates a guard that fails when the predicate is true.

When to use it:

- Use it for explicit deny conditions that are clearer than inverting an allow predicate.

Classification: Advanced

Example: [Example denyIf](#example-denyif)

### hasCapability

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
function hasCapability<TInput extends { actor: Actor }, TCapability extends Capability, TReason extends object>(
  capability: TCapability,
  reason: TReason | ((input: TInput) => TReason),
): Guard<TInput, TReason>;
```

What it is for:

- Checks whether the actor carries a given capability.

When to use it:

- Use it inside shared policies, not as scattered application logic.

Classification: Primary

Example: [Example hasCapability](#example-hascapability)

### isAuthenticated

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
function isAuthenticated<TInput extends { actor: Actor }, TReason extends object>(
  reason: TReason | ((input: TInput) => TReason),
): Guard<TInput, TReason>;
```

What it is for:

- Checks that the actor is authenticated before later guards run.

When to use it:

- Use it as the first guard in policies that require a signed-in actor.

Classification: Primary

Example: [Example isAuthenticated](#example-isauthenticated)

### createClerkPbacClient

Import path:

- `@gianstack/next-clerk-pbac/client`

Type:

```ts
function createClerkPbacClient<TConfig extends ClerkPbacConfig>(
  config: TConfig,
): {
  useActorState(): ActorState<ActorOfConfig<TConfig>>;
};
```

What it is for:

- Builds the client boundary adapter for a shared PBAC config.

When to use it:

- Use it in a dedicated client module that creates the adapter once for a shared `pbac` config.
- Consumer apps will usually wrap `pbacClient.useActorState()` in a local hook such as `useAppActorState()`.

Classification: Primary

Example: [Example createClerkPbacClient](#example-createclerkpbacclient)

### createClerkPbacServer

Import path:

- `@gianstack/next-clerk-pbac/server`

Type:

```ts
function createClerkPbacServer<TConfig extends ClerkPbacConfig>(
  config: TConfig,
): {
  getActor(authOptions?: Parameters<typeof auth>[0]): Promise<ActorOfConfig<TConfig>>;
};
```

What it is for:

- Builds the server boundary adapter for a shared PBAC config.

When to use it:

- Use it at the real enforcement boundary before evaluating a policy.

Classification: Primary

Example: [Example createClerkPbacServer](#example-createclerkpbacserver)

### AccessOfPolicy

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type AccessOfPolicy<TPolicy> =
  TPolicy extends (...args: never[]) => AuthorizationDecision<infer TAccess, object>
    ? TAccess
    : never;
```

What it is for:

- Derives the allowed access union from a policy.

When to use it:

- Use it when downstream code wants to type branches around a specific policy’s success modes.

Classification: Primary

Example: [Example AccessOfPolicy](#example-accessofpolicy)

### AllowedAuthorizationDecision

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type AllowedAuthorizationDecision<TAccess = unknown> = {
  kind: "allowed";
  access: TAccess;
};
```

What it is for:

- Represents the successful branch of an authorization result with a user-defined access payload.

When to use it:

- Use it when downstream code only accepts allowed decisions, or when you want to make the role of `TAccess` explicit in your own types.
- `TAccess` is defined by the consumer, not by the package.

Classification: Primary

Example: [Example AllowedAuthorizationDecision](#example-allowedauthorizationdecision)

### Actor

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type Actor<TCapability extends Capability = Capability, TRole extends string = string> =
  | {
      authenticated: false;
      userId: null;
      organization: null;
      capabilities: readonly TCapability[];
    }
  | {
      authenticated: true;
      userId: string;
      organization: null | { id: string; role: TRole | null };
      capabilities: readonly TCapability[];
    };
```

What it is for:

- Represents the normalized application-facing actor shape used by policies.

When to use it:

- Use it for helper utilities or shared types that work on actors independently of a concrete config.

Classification: Primary

Example: [Example Actor](#example-actor)

### ActorState

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type ActorState<TActor> =
  | {
      status: "pending";
      isPending: true;
      isReady: false;
      actor: null;
    }
  | {
      status: "ready";
      isPending: false;
      isReady: true;
      actor: TActor;
    };
```

What it is for:

- Models the client-side actor resolution state.
- Keeps `status`, `isPending`, `isReady`, and `actor` on stable keys so components can destructure the hook result without losing TypeScript narrowing.

When to use it:

- Use it when you want a typed wrapper around `pbacClient.useActorState()` or when a component API accepts the client actor state directly.

Classification: Primary

Example: [Example ActorState](#example-actorstate)

### ActorOfConfig

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type ActorOfConfig<TConfig extends ClerkPbacConfig> = Actor<
  CapabilityOfConfig<TConfig>,
  RoleOfConfig<TConfig>
>;
```

What it is for:

- Derives the concrete actor type from a PBAC config.

When to use it:

- Use it when you want app-specific actor types without repeating capability and role unions manually.

Classification: Primary

Example: [Example ActorOfConfig](#example-actorofconfig)

### AuthorizationDecision

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type AllowedAuthorizationDecision<TAccess = unknown> = {
  kind: "allowed";
  access: TAccess;
};

type AuthorizationDecision<TAccess = unknown, TReason extends object = object> =
  | {
      kind: "denied";
      reasons: readonly TReason[];
    }
  | AllowedAuthorizationDecision<TAccess>;
```

What it is for:

- Represents the union returned by every policy evaluation.

When to use it:

- Use it when downstream code accepts or returns a typed authorization result.
- `TAccess` is whatever success payload your application wants to propagate.
- `TReason` controls the denied branch.

Classification: Primary

Example: [Example AuthorizationDecision](#example-authorizationdecision)

### Capability

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type Capability = `${string}.${string}`;
```

What it is for:

- Represents the string shape of a capability.

When to use it:

- Use it when a helper accepts arbitrary capabilities without depending on a concrete config.

Classification: Advanced

Example: [Example Capability](#example-capability)

### CapabilityCatalog

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type CapabilityCatalog<TDefinition extends CapabilityDefinition> = {
  readonly [TResource in keyof TDefinition]: {
    readonly [TAction in TDefinition[TResource][number]]: `${Extract<TResource, string>}.${Extract<TAction, string>}`;
  };
};
```

What it is for:

- Represents the typed object returned by `defineCapabilities`.

When to use it:

- Use it when you need to type a function around a known capability definition.

Classification: Advanced

Example: [Example CapabilityCatalog](#example-capabilitycatalog)

### CapabilityCatalogShape

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type CapabilityCatalogShape = Record<string, Record<string, Capability>>;
```

What it is for:

- Describes the structural shape of any generated capability catalog.

When to use it:

- Use it in generic helpers that accept any capability catalog.

Classification: Advanced

Example: [Example CapabilityCatalogShape](#example-capabilitycatalogshape)

### CapabilityDefinition

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type CapabilityDefinition = Record<string, readonly string[]>;
```

What it is for:

- Describes the raw input shape accepted by `defineCapabilities`.

When to use it:

- Use it when you want to type a helper that builds or validates catalog definitions before calling `defineCapabilities`.

Classification: Advanced

Example: [Example CapabilityDefinition](#example-capabilitydefinition)

### CapabilityOfCatalog

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type CapabilityOfCatalog<TCatalog extends CapabilityCatalogShape> = ...;
```

What it is for:

- Derives the concrete capability union from a capability catalog.

When to use it:

- Use it when typing `roleCapabilities` before the `createClerkPbac(...)` call.
- Prefer it over a hand-written capability union when the source of truth is already the generated catalog.

Classification: Advanced

Example: [Example CapabilityOfCatalog](#example-capabilityofcatalog)

### CapabilityOfConfig

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type CapabilityOfConfig<TConfig extends ClerkPbacConfig> = ...;
```

What it is for:

- Derives the concrete capability union from a config.

When to use it:

- Use it when a helper should only accept capabilities that belong to a specific `pbac`.

Classification: Advanced

Example: [Example CapabilityOfConfig](#example-capabilityofconfig)

### ClerkPbacConfig

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
interface ClerkPbacConfig<
  TCapability extends Capability = Capability,
  TKnownRoles extends readonly string[] = readonly string[],
  TCapabilities extends CapabilityCatalogShape = CapabilityCatalogShape,
> {
  readonly capabilities: TCapabilities;
  readonly roles: TKnownRoles;
  readonly roleCapabilities: RoleCapabilitiesMap<
    Extract<TKnownRoles[number], string>,
    TCapability
  >;
  readonly onUnknownRole?: (event: {
    source: "server" | "client";
    role: string;
    organizationId: string | null;
    userId: string | null;
    roles: readonly Extract<TKnownRoles[number], string>[];
  }) => void;
  resolveCapabilities(
    input: ResolveCapabilitiesInput<
      TCapability,
      Extract<TKnownRoles[number], string>
    >,
  ): readonly TCapability[];
}
```

What it is for:

- Represents the shared config returned by `createClerkPbac`.

When to use it:

- Use it in helpers or generics that operate on any PBAC config.
- Treat `roles` as the full list of Clerk roles declared for the app, while `roleCapabilities` stays the subset that grants base capabilities.

Classification: Advanced

Example: [Example ClerkPbacConfig](#example-clerkpbacconfig)

### Guard

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type Guard<TInput, TReason extends object = object> = (
  input: TInput,
) => readonly TReason[];
```

What it is for:

- Represents a reusable check that returns zero or more denial reasons.

When to use it:

- Use it when you want to create custom building blocks that plug into policy composition.

Classification: Advanced

Example: [Example Guard](#example-guard)

### Policy

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type Policy<TInput, TAccess = unknown, TReason extends object = object> =
  (input: TInput) => AuthorizationDecision<TAccess, TReason>;
```

What it is for:

- Represents a reusable authorization policy.

When to use it:

- Use it when a helper accepts or returns a full policy.

Classification: Primary

Example: [Example Policy](#example-policy)

### ReasonOfPolicy

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type ReasonOfPolicy<TPolicy> =
  TPolicy extends (...args: never[]) => AuthorizationDecision<unknown, infer TReason>
    ? TReason
    : never;
```

What it is for:

- Derives the denial reason union from a policy.

When to use it:

- Use it when downstream code wants typed denial handling for a specific policy.

Classification: Primary

Example: [Example ReasonOfPolicy](#example-reasonofpolicy)

### ResolveCapabilitiesInput

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
interface ResolveCapabilitiesInput<TCapability extends Capability = Capability, TRole extends string = string> {
  authenticated: boolean;
  userId: string | null;
  organization: null | { id: string; role: TRole | null };
  baseCapabilities: readonly TCapability[];
  roleCapabilities: RoleCapabilitiesMap<TRole, TCapability>;
}
```

What it is for:

- Represents the input passed to `resolveCapabilities`.

When to use it:

- Use it only for rare, trusted, already-available contextual derivation at actor-resolution time.
- Unknown Clerk roles have already been normalized to `organization.role = null` before this hook runs.

Classification: Advanced

Example: [Example ResolveCapabilitiesInput](#example-resolvecapabilitiesinput)

### RoleCapabilitiesMap

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type RoleCapabilitiesMap<
  TRole extends string,
  TCapability extends Capability,
> = Readonly<Partial<Record<TRole, readonly TCapability[]>>>;
```

What it is for:

- Constrains `roleCapabilities` so its keys come from your declared Clerk roles and its values come from your capability union.

When to use it:

- Use it with `satisfies` when declaring `roleCapabilities`, before you pass the config into `createClerkPbac(...)`.

Classification: Advanced

Example: [Example RoleCapabilitiesMap](#example-rolecapabilitiesmap)

### RoleOfConfig

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type RoleOfConfig<TConfig extends ClerkPbacConfig> = ...;
```

What it is for:

- Derives the organization role union from `pbac.roles`.

When to use it:

- Use it when a helper needs to stay aligned with the full set of Clerk roles declared in your `pbac`, not only the mapped subset.

Classification: Advanced

Example: [Example RoleOfConfig](#example-roleofconfig)

### Rule

Import path:

- `@gianstack/next-clerk-pbac`

Type:

```ts
type Rule<TInput, TAccess = unknown, TReason extends object = object> =
  Policy<TInput, TAccess, TReason>;
```

What it is for:

- Represents the callable rule that `createPolicy()` wraps.

When to use it:

- Use it when you want to name a rule before exporting it as a policy.

Classification: Advanced

Example: [Example Rule](#example-rule)

## Examples

These examples are intentionally small and focused. The runtime ones are realistic; the pure type ones stay minimal and concrete.

### Example defineCapabilities

API reference: [defineCapabilities](#definecapabilities)

```ts
import { defineCapabilities } from "@gianstack/next-clerk-pbac";

// Turn a static definition into a typed immutable catalog.
const capabilities = defineCapabilities({
  dashboard: ["access"],
  post: ["readOwn", "updateOwn"],
} as const);

capabilities.dashboard.access;
capabilities.post.updateOwn;
```

### Example createClerkPbac

API reference: [createClerkPbac](#createclerkpbac)

```ts
import {
  createClerkPbac,
  defineCapabilities,
  type CapabilityOfCatalog,
  type RoleCapabilitiesMap,
} from "@gianstack/next-clerk-pbac";

const capabilities = defineCapabilities({
  dashboard: ["access"],
  post: ["readOwn"],
} as const);
const roles = ["org:admin", "org:member"] as const;
type AppRole = (typeof roles)[number];
type AppCapability = CapabilityOfCatalog<typeof capabilities>;
const roleCapabilities = {
  "org:admin": [capabilities.dashboard.access, capabilities.post.readOwn],
  "org:member": [capabilities.post.readOwn],
} as const satisfies RoleCapabilitiesMap<AppRole, AppCapability>;

// The PBAC config is the shared object reused across the app.
export const pbac = createClerkPbac({
  capabilities,
  roles,
  roleCapabilities,
});
```

### Example allowIf

API reference: [allowIf](#allowif)

```ts
import { allowIf } from "@gianstack/next-clerk-pbac";

// This guard passes only when the post is already published.
const isPublished = allowIf(
  (input: { published: boolean }) => input.published,
  {
    code: "POST_NOT_PUBLISHED",
    message: "The post must be published first.",
  },
);
```

### Example allowIfAll

API reference: [allowIfAll](#allowifall)

```ts
import {
  allowIfAll,
  hasCapability,
  isAuthenticated,
} from "@gianstack/next-clerk-pbac";

declare const pbac: {
  capabilities: {
    post: {
      updateOwn: "post.updateOwn";
    };
  };
};

// Both conditions must pass for the policy to allow.
const updateOwnPolicy = allowIfAll(
  { access: "own" },
  isAuthenticated({
    code: "NOT_AUTHENTICATED",
    message: "You must be authenticated.",
  }),
  hasCapability(pbac.capabilities.post.updateOwn, {
    code: "MISSING_UPDATE_OWN",
    message: "Missing capability post.updateOwn.",
  }),
);
```

### Example allowIfAny

API reference: [allowIfAny](#allowifany)

```ts
import {
  allowIfAll,
  allowIfAny,
  hasCapability,
  isAuthenticated,
} from "@gianstack/next-clerk-pbac";

declare const pbac: {
  capabilities: {
    post: {
      updateAny: "post.updateAny";
      updateOwn: "post.updateOwn";
    };
  };
};

// The caller can be allowed through either the "any" path or the "own" path.
const updatePostPolicy = allowIfAny(
  allowIfAll(
    { access: "any" },
    isAuthenticated({
      code: "NOT_AUTHENTICATED",
      message: "You must be authenticated.",
    }),
    hasCapability(pbac.capabilities.post.updateAny, {
      code: "MISSING_UPDATE_ANY",
      message: "Missing capability post.updateAny.",
    }),
  ),
  allowIfAll(
    { access: "own" },
    isAuthenticated({
      code: "NOT_AUTHENTICATED",
      message: "You must be authenticated.",
    }),
    hasCapability(pbac.capabilities.post.updateOwn, {
      code: "MISSING_UPDATE_OWN",
      message: "Missing capability post.updateOwn.",
    }),
  ),
);
```

### Example authorize

API reference: [authorize](#authorize)

```ts
import { authorize } from "@gianstack/next-clerk-pbac";
import type { Actor } from "@gianstack/next-clerk-pbac";

declare const updatePostPolicy: (input: { actor: Actor }) =>
  | {
      kind: "allowed";
      access: "own";
    }
  | {
      kind: "denied";
      reasons: readonly { code: string; message: string }[];
    };

// `authorize` just evaluates the policy with the input you provide.
const decision = authorize(updatePostPolicy, {
  actor: {
    authenticated: true,
    userId: "user_123",
    organization: null,
    capabilities: [],
  },
});
```

### Example check

API reference: [check](#check)

```ts
import { check } from "@gianstack/next-clerk-pbac";
import type { Actor } from "@gianstack/next-clerk-pbac";

// `check` helps name a predicate so you can reuse it elsewhere.
const isOwner = check(
  (input: { actor: Actor; ownerId: string }) =>
    input.actor.authenticated && input.actor.userId === input.ownerId,
);
```

### Example createPolicy

API reference: [createPolicy](#createpolicy)

```ts
import {
  allowIfAll,
  createPolicy,
  hasCapability,
  isAuthenticated,
} from "@gianstack/next-clerk-pbac";

declare const pbac: {
  capabilities: {
    dashboard: {
      access: "dashboard.access";
    };
  };
};

// Wrap the rule as a named exported policy.
export const readDashboardPolicy = createPolicy(
  allowIfAll(
    { access: "any" },
    isAuthenticated({
      code: "NOT_AUTHENTICATED",
      message: "You must be authenticated.",
    }),
    hasCapability(pbac.capabilities.dashboard.access, {
      code: "MISSING_DASHBOARD_ACCESS",
      message: "Missing capability dashboard.access.",
    }),
  ),
);
```

### Example denyIf

API reference: [denyIf](#denyif)

```ts
import { denyIf } from "@gianstack/next-clerk-pbac";

// This guard denies access when the resource is archived.
const denyArchivedPosts = denyIf(
  (input: { archived: boolean }) => input.archived,
  {
    code: "POST_ARCHIVED",
    message: "Archived posts cannot be edited.",
  },
);
```

### Example hasCapability

API reference: [hasCapability](#hascapability)

```ts
import { hasCapability } from "@gianstack/next-clerk-pbac";

declare const capability: "dashboard.access";

// Use capability checks inside shared policies, not ad hoc in app code.
const hasDashboardAccess = hasCapability(capability, {
  code: "MISSING_DASHBOARD_ACCESS",
  message: "Missing capability dashboard.access.",
});
```

### Example isAuthenticated

API reference: [isAuthenticated](#isauthenticated)

```ts
import { isAuthenticated } from "@gianstack/next-clerk-pbac";

// Start policies with the auth gate when the actor must be signed in.
const requireAuth = isAuthenticated({
  code: "NOT_AUTHENTICATED",
  message: "You must be authenticated.",
});
```

### Example createClerkPbacClient

API reference: [createClerkPbacClient](#createclerkpbacclient)

```tsx
// pbac.ts
import {
  createClerkPbac,
  defineCapabilities,
  type ActorOfConfig,
  type ActorState,
  type CapabilityOfCatalog,
  type RoleCapabilitiesMap,
} from "@gianstack/next-clerk-pbac";

const capabilities = defineCapabilities({
  dashboard: ["access"],
  post: ["readOwn"],
} as const);
const roles = ["org:admin", "org:member"] as const;
type AppRole = (typeof roles)[number];
type AppCapability = CapabilityOfCatalog<typeof capabilities>;
const roleCapabilities = {
  "org:admin": [capabilities.dashboard.access, capabilities.post.readOwn],
  "org:member": [capabilities.post.readOwn],
} as const satisfies RoleCapabilitiesMap<AppRole, AppCapability>;

export const pbac = createClerkPbac({
  capabilities,
  roles,
  roleCapabilities,
});

export type AppActor = ActorOfConfig<typeof pbac>;
export type AppActorState = ActorState<AppActor>;
```

```tsx
// pbac-client.ts
"use client";

import { createClerkPbacClient } from "@gianstack/next-clerk-pbac/client";

import { pbac, type AppActorState } from "./pbac";

// Create the client adapter once in a client module.
const pbacClient = createClerkPbacClient(pbac);

// Re-export one app-level hook with the shared AppActorState alias.
export function useAppActorState(): AppActorState {
  return pbacClient.useActorState();
}
```

```tsx
// dashboard-gate.tsx
"use client";

import { useAppActorState } from "./pbac-client";

export function DashboardGate() {
  const { isPending, actor } = useAppActorState();

  if (isPending) {
    return null;
  }

  return <span>{String(actor.authenticated)}</span>;
}
```

### Example createClerkPbacServer

API reference: [createClerkPbacServer](#createclerkpbacserver)

```ts
import { createClerkPbacServer } from "@gianstack/next-clerk-pbac/server";

import { pbac } from "./pbac";

// Create the server adapter once in a server-only module.
const pbacServer = createClerkPbacServer(pbac);

export async function loadActorForRoute() {
  return pbacServer.getActor();
}
```

### Example AccessOfPolicy

API reference: [AccessOfPolicy](#accessofpolicy)

```ts
import type { AccessOfPolicy } from "@gianstack/next-clerk-pbac";

declare const updatePostPolicy: (
  input: unknown,
) =>
  | {
      kind: "allowed";
      access: "own";
    }
  | {
      kind: "allowed";
      access: "any";
    }
  | {
      kind: "denied";
      reasons: readonly { code: string; message: string }[];
    };

type UpdatePostAccess = AccessOfPolicy<typeof updatePostPolicy>;
```

### Example AllowedAuthorizationDecision

API reference: [AllowedAuthorizationDecision](#allowedauthorizationdecision)

```ts
import type {
  AllowedAuthorizationDecision,
} from "@gianstack/next-clerk-pbac";

type MembershipAccess =
  | "any"
  | { kind: "organizations"; organizationIds: readonly string[] };

type SuccessfulDecision = AllowedAuthorizationDecision<MembershipAccess>;

function readAllowedScope(decision: SuccessfulDecision) {
  return decision.access;
}
```

### Example Actor

API reference: [Actor](#actor)

```ts
import type { Actor } from "@gianstack/next-clerk-pbac";

function describeActor(actor: Actor) {
  if (!actor.authenticated) {
    return "anonymous";
  }

  return actor.organization
    ? `${actor.userId} in ${actor.organization.id}`
    : actor.userId;
}
```

### Example ActorState

API reference: [ActorState](#actorstate)

```tsx
// pbac.ts
import {
  createClerkPbac,
  defineCapabilities,
  type ActorOfConfig,
  type ActorState,
  type CapabilityOfCatalog,
  type RoleCapabilitiesMap,
} from "@gianstack/next-clerk-pbac";

const capabilities = defineCapabilities({
  dashboard: ["access"],
  post: ["readOwn"],
} as const);
const roles = ["org:admin", "org:member"] as const;
type AppRole = (typeof roles)[number];
type AppCapability = CapabilityOfCatalog<typeof capabilities>;
const roleCapabilities = {
  "org:admin": [capabilities.dashboard.access, capabilities.post.readOwn],
  "org:member": [capabilities.post.readOwn],
} as const satisfies RoleCapabilitiesMap<AppRole, AppCapability>;

export const pbac = createClerkPbac({
  capabilities,
  roles,
  roleCapabilities,
});

export type AppActor = ActorOfConfig<typeof pbac>;
export type AppActorState = ActorState<AppActor>;
```

```tsx
// pbac-client.ts
"use client";

import { createClerkPbacClient } from "@gianstack/next-clerk-pbac/client";

import { pbac, type AppActorState } from "./pbac";

const pbacClient = createClerkPbacClient(pbac);

export function useAppActorState(): AppActorState {
  // Return the shared AppActorState alias from one app-level hook.
  return pbacClient.useActorState();
}
```

```tsx
// dashboard-gate.tsx
"use client";

import { useAppActorState } from "./pbac-client";

export function DashboardGate() {
  const { isPending, actor } = useAppActorState();

  if (isPending) {
    return null;
  }

  return <span>{String(actor.authenticated)}</span>;
}
```

### Example ActorOfConfig

API reference: [ActorOfConfig](#actorofconfig)

```ts
import type { ActorOfConfig } from "@gianstack/next-clerk-pbac";

import { pbac } from "./pbac";

type AppActor = ActorOfConfig<typeof pbac>;

function readActorUserId(actor: AppActor) {
  return actor.userId;
}
```

### Example AuthorizationDecision

API reference: [AuthorizationDecision](#authorizationdecision)

```ts
import type { AuthorizationDecision } from "@gianstack/next-clerk-pbac";

type MembershipAccess =
  | "any"
  | { kind: "organizations"; organizationIds: readonly string[] };

type Decision = AuthorizationDecision<MembershipAccess, { code: string; message: string }>;

function describeDecision(decision: Decision) {
  if (decision.kind === "denied") {
    return decision.reasons.map((reason) => reason.code);
  }

  // The allowed branch always carries the user-defined access payload.
  return decision.access;
}
```

### Example Capability

API reference: [Capability](#capability)

```ts
import type { Capability } from "@gianstack/next-clerk-pbac";

const capability: Capability = "post.updateOwn";
```

### Example CapabilityCatalog

API reference: [CapabilityCatalog](#capabilitycatalog)

```ts
import type { CapabilityCatalog } from "@gianstack/next-clerk-pbac";

const definition = {
  dashboard: ["access"],
  post: ["readOwn"],
} as const;

type AppCatalog = CapabilityCatalog<typeof definition>;
```

### Example CapabilityCatalogShape

API reference: [CapabilityCatalogShape](#capabilitycatalogshape)

```ts
import type { CapabilityCatalogShape } from "@gianstack/next-clerk-pbac";

function listResources(catalog: CapabilityCatalogShape) {
  return Object.keys(catalog);
}
```

### Example CapabilityDefinition

API reference: [CapabilityDefinition](#capabilitydefinition)

```ts
import type { CapabilityDefinition } from "@gianstack/next-clerk-pbac";

const definition: CapabilityDefinition = {
  dashboard: ["access"],
  post: ["readOwn", "updateOwn"],
};
```

### Example CapabilityOfCatalog

API reference: [CapabilityOfCatalog](#capabilityofcatalog)

```ts
import {
  defineCapabilities,
  type CapabilityOfCatalog,
} from "@gianstack/next-clerk-pbac";

const capabilities = defineCapabilities({
  dashboard: ["access"],
  post: ["readOwn"],
} as const);

type AppCapability = CapabilityOfCatalog<typeof capabilities>;

const fallbackCapability: AppCapability = "post.readOwn";
```

### Example CapabilityOfConfig

API reference: [CapabilityOfConfig](#capabilityofconfig)

```ts
import type { CapabilityOfConfig } from "@gianstack/next-clerk-pbac";

import { pbac } from "./pbac";

type AppCapability = CapabilityOfConfig<typeof pbac>;

const fallbackCapability: AppCapability = "post.readOwn";
```

### Example ClerkPbacConfig

API reference: [ClerkPbacConfig](#clerkpbacconfig)

```ts
import type { ClerkPbacConfig } from "@gianstack/next-clerk-pbac";

function readRoleCapabilities(config: ClerkPbacConfig) {
  return {
    roles: config.roles,
    roleCapabilities: config.roleCapabilities,
  };
}
```

### Example Guard

API reference: [Guard](#guard)

```ts
import type { Guard } from "@gianstack/next-clerk-pbac";

const requireSlug: Guard<{ slug?: string }, { code: string; message: string }> = (
  input,
) =>
  input.slug
    ? []
    : [
        {
          code: "MISSING_SLUG",
          message: "A slug is required.",
        },
      ];
```

### Example Policy

API reference: [Policy](#policy)

```ts
import type { Policy } from "@gianstack/next-clerk-pbac";
import type { Actor } from "@gianstack/next-clerk-pbac";

const readPolicy: Policy<
  { actor: Actor },
  "any",
  { code: string; message: string }
> = (input) =>
  input.actor.authenticated
    ? {
        kind: "allowed",
        access: "any",
      }
    : {
        kind: "denied",
        reasons: [
          {
            code: "NOT_AUTHENTICATED",
            message: "You must be authenticated.",
          },
        ],
      };
```

### Example ReasonOfPolicy

API reference: [ReasonOfPolicy](#reasonofpolicy)

```ts
import type { ReasonOfPolicy } from "@gianstack/next-clerk-pbac";

declare const updatePostPolicy: (
  input: unknown,
) =>
  | {
      kind: "allowed";
      access: "own";
    }
  | {
      kind: "denied";
      reasons: readonly { code: string; message: string }[];
    };

type UpdatePostReason = ReasonOfPolicy<typeof updatePostPolicy>;
```

### Example ResolveCapabilitiesInput

API reference: [ResolveCapabilitiesInput](#resolvecapabilitiesinput)

```ts
import {
  createClerkPbac,
  defineCapabilities,
  type CapabilityOfCatalog,
  type ResolveCapabilitiesInput,
  type RoleCapabilitiesMap,
} from "@gianstack/next-clerk-pbac";

const capabilities = defineCapabilities({
  dashboard: ["access"],
  post: ["readAny", "readOwn", "updateAny", "updateOwn"],
} as const);
const roles = ["org:admin", "org:member"] as const;
type AppRole = (typeof roles)[number];
type AppCapability = CapabilityOfCatalog<typeof capabilities>;

const roleCapabilities = {
  "org:admin": [
    capabilities.dashboard.access,
    capabilities.post.readAny,
    capabilities.post.updateAny,
  ],
  "org:member": [
    capabilities.post.readOwn,
    capabilities.post.updateOwn,
  ],
} as const satisfies RoleCapabilitiesMap<AppRole, AppCapability>;

const appConfig = {
  readOnlyMode: true,
};

const resolveCapabilities = ({
  baseCapabilities,
  organization,
}: ResolveCapabilitiesInput<AppCapability, AppRole>) => {
  // Example 1: a trusted app-level switch narrows write capabilities.
  let effectiveCapabilities = baseCapabilities;

  if (appConfig.readOnlyMode) {
    effectiveCapabilities = effectiveCapabilities.filter(
      (capability) =>
        capability !== capabilities.post.updateAny &&
        capability !== capabilities.post.updateOwn,
    );
  }

  // Example 2: a suspended organization can be narrowed to no capabilities.
  if (organization?.id === "org_suspended") {
    return [];
  }

  return effectiveCapabilities;
};

export const pbac = createClerkPbac({
  capabilities,
  roles,
  roleCapabilities,
  resolveCapabilities,
});
```

### Example RoleOfConfig

API reference: [RoleOfConfig](#roleofconfig)

```ts
import type { RoleOfConfig } from "@gianstack/next-clerk-pbac";

import { pbac } from "./pbac";

type AppRole = RoleOfConfig<typeof pbac>;

const defaultRole: AppRole = "org:member";
const adminRole: AppRole = "org:admin";
```

### Example RoleCapabilitiesMap

API reference: [RoleCapabilitiesMap](#rolecapabilitiesmap)

```ts
import type {
  CapabilityOfCatalog,
  RoleCapabilitiesMap,
} from "@gianstack/next-clerk-pbac";
import { defineCapabilities } from "@gianstack/next-clerk-pbac";

const capabilities = defineCapabilities({
  dashboard: ["access"],
  post: ["readOwn"],
} as const);

const roles = ["org:admin", "org:member"] as const;
type AppRole = (typeof roles)[number];
type AppCapability = CapabilityOfCatalog<typeof capabilities>;

const roleCapabilities = {
  "org:admin": [capabilities.dashboard.access],
  "org:member": [],
} as const satisfies RoleCapabilitiesMap<AppRole, AppCapability>;
```

### Example Rule

API reference: [Rule](#rule)

```ts
import type { Rule } from "@gianstack/next-clerk-pbac";
import type { Actor } from "@gianstack/next-clerk-pbac";

const readRule: Rule<
  { actor: Actor },
  "any",
  { code: string; message: string }
> = (input) =>
  input.actor.authenticated
    ? {
        kind: "allowed",
        access: "any",
      }
    : {
        kind: "denied",
        reasons: [
          {
            code: "NOT_AUTHENTICATED",
            message: "You must be authenticated.",
          },
        ],
      };
```