npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gianstack/next-clerk-pbac

v0.2.0

Published

A publishable Next.js App Router + Clerk package for PBAC capability catalogs, actor resolution, and synchronous authorization policies.

Downloads

229

Readme

@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

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:

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

An app with richer authorization semantics may use objects instead:

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.

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, CapabilityDefinition, 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.

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, CapabilityOfCatalog, 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.

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, 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.

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, allowIfAll, allowIfAny, isAuthenticated, hasCapability, 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.

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, Actor, 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.

// 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();
}
// 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, ActorState, 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.

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, ActorOfConfig, AuthorizationDecision

Step 8 Branch on the decision union

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

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, AuthorizationDecision, AccessOfPolicy, ReasonOfPolicy

API Reference

defineCapabilities

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

createClerkPbac

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

allowIf

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

allowIfAll

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

allowIfAny

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

authorize

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

check

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

createPolicy

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

denyIf

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

hasCapability

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

isAuthenticated

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

createClerkPbacClient

Import path:

  • @gianstack/next-clerk-pbac/client

Type:

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

createClerkPbacServer

Import path:

  • @gianstack/next-clerk-pbac/server

Type:

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

AccessOfPolicy

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

AllowedAuthorizationDecision

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

Actor

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

ActorState

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

ActorOfConfig

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

AuthorizationDecision

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

Capability

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

CapabilityCatalog

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

CapabilityCatalogShape

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

CapabilityDefinition

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

CapabilityOfCatalog

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

CapabilityOfConfig

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

ClerkPbacConfig

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

Guard

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

Policy

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

ReasonOfPolicy

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

ResolveCapabilitiesInput

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

RoleCapabilitiesMap

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

RoleOfConfig

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

Rule

Import path:

  • @gianstack/next-clerk-pbac

Type:

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

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

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

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

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

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

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

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

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

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

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

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

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

// 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>;
// 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();
}
// 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

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

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

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

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

// 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>;
// 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();
}
// 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

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

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

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

const capability: Capability = "post.updateOwn";

Example CapabilityCatalog

API reference: CapabilityCatalog

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

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

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

Example CapabilityDefinition

API reference: CapabilityDefinition

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

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

Example CapabilityOfCatalog

API reference: CapabilityOfCatalog

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

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

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

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

Example Guard

API reference: Guard

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

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

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

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

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

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

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.",
          },
        ],
      };