# Feature Policies API

> Build declarative business rules with MakerKit's Feature Policies API. Validate team invitations, enforce subscription limits, and create custom authorization flows.

*Canonical: https://makerkit.dev/docs/tanstack-supabase/api/policies-api*

---

The Feature Policy API isolates validation and authorization logic from application code so every feature can reuse consistent, auditable policies.

**Makerkit is built for extensibility**: customers should expand features without patching internals unless they are opting into special cases. The Feature Policy API delivers that promise by turning customization into additive policies instead of edits to core flows.

**Important**: Feature Policies operates at the API and application surface level. It orchestrates business logic, user experience flows, and feature access decisions. For data integrity and security enforcement, **continue using database constraints, Supabase RLS policies, and transactional safeguards as your source of truth**.

## What It's For

- **Application logic**: User flows, feature access, business rule validation
- **API orchestration**: Request processing, workflow coordination, conditional routing
- **User experience**: Dynamic UI behavior, progressive disclosure, personalization
- **Integration patterns**: Third-party service coordination, webhook processing

## What It's NOT For

- **Data integrity**: Use database constraints and foreign keys
- **Security enforcement**: Use Supabase RLS policies and authentication
- **Performance-critical paths**: Use database indexes and query optimization
- **Transactional consistency**: Use database transactions and ACID guarantees

## Key Benefits

- Apply nuanced rules without coupling them to route handlers or services
- Share policy logic across server actions, mutations, and background jobs
- Test policies in isolation while keeping runtime orchestration predictable
- Layer customer-specific extensions on top of Makerkit defaults

## How We Use It Today

Makerkit uses the Feature Policy API in two team-account flows:

- **Team invitations** - validates **when a team can send invitations**. The registry lives in `packages/features/team-accounts/src/server/policies/policies.ts` and is evaluated by `createInvitationsPolicyEvaluator()` in `invitation-policies.ts` in the same folder.
- **Team account creation** - validates when a user can create a new team account. The registry lives in `packages/features/team-accounts/src/server/policies/create-account-policies.server.ts` and is evaluated by `createAccountCreationPolicyEvaluator()` in `create-account-policy-evaluator.server.ts`.

**Both registries ship empty: no policy is registered by default**, so invitations and team creation are allowed until you register your own rules. The kit defines one ready-made invitation policy (`subscriptionRequiredInvitationsPolicy`) that you can opt into with a single line (see [Real-World Example](#real-world-example-team-invitations)).

While supporting customers implement various flows for invitations, it was clear that the SaaS Starter Kit could not assume what rules you want to apply to invitations.

- Some customers wanted to validate the email address of the invited user (ex. validate they all shared the same domain)
- A set of customers wanted only users on a specific plan to be able to invite users (ex. only Pro users can invite users)
- Others simply wanted to limit how many invitations can be sent on a per-plan basis (ex. only 5 invitations can be sent on a free plan, 20 on a paid plan, etc.)

These rules required a more declarative approach - which is why we created the Policies API - so that users can layer their own requirements without the need to rewrite internals.

Additional features can opt in to the same registry pattern to unlock the shared orchestration and extension tooling.

## Why Feature Policies?

A SaaS starter kit must adapt to **diverse customer requirements** without creating divergent forks. 

Imperative checks embedded in controllers quickly become brittle: every variation requires new conditionals, feature flags, or early returns scattered across files.

The Feature Policy API keeps the rule set declarative and centralized, **so product teams can swap, reorder, or extend policies without rewriting the baseline flow**. 

Registries turn policy changes into configuration instead of refactors, making it safer for customers to customize logic while continuing to receive upstream updates from Makerkit.

## Overview

The Feature Policy API provides:

- **Feature-specific registries** for organized policy management per feature
- **Configuration support** so policies can accept typed configuration objects
- **Stage-aware evaluation** enabling policies to be filtered by execution stage
- **Immutable contexts** that keep policy execution safe and predictable
- **Perfect DX** through a unified API that just works

## Quick Start

All building blocks are exported from `@kit/policies` (source in `packages/policies/src`). The examples in this section are policies **you add yourself** - they are not part of the kit.

### 1. Define Policies in a Feature Registry

The simplest way to add an invitation rule is to define it in the kit's existing invitation registry file and register it there:

```typescript {% title="packages/features/team-accounts/src/server/policies/policies.ts" %}
import * as z from 'zod';

import { allow, createPolicyRegistry, definePolicy, deny } from '@kit/policies';

import type { FeaturePolicyInvitationContext } from './feature-policy-invitation-context';

// Already exported by the kit
export const invitationPolicyRegistry = createPolicyRegistry();

// Example policy you add yourself
export const emailValidationPolicy =
  definePolicy<FeaturePolicyInvitationContext>({
    id: 'email-validation',
    stages: ['submission'],
    evaluate: async ({ invitations }) => {
      if (invitations.some((invitation) => !invitation.email.includes('@'))) {
        return deny({
          code: 'INVALID_EMAIL_FORMAT',
          message: 'Invalid email format',
          remediation: 'Please provide a valid email address',
        });
      }

      return allow();
    },
  });

// Example configurable policy you add yourself
export const maxInvitationsPolicy = definePolicy<
  FeaturePolicyInvitationContext,
  { maxInvitations: number }
>({
  id: 'max-invitations',
  stages: ['submission'],
  configSchema: z.object({
    maxInvitations: z.number().positive(),
  }),
  evaluate: async ({ invitations }, config = { maxInvitations: 5 }) => {
    if (invitations.length > config.maxInvitations) {
      return deny({
        code: 'MAX_INVITATIONS_EXCEEDED',
        message: `Cannot invite more than ${config.maxInvitations} members`,
        remediation: `Reduce invitations to ${config.maxInvitations} or fewer`,
      });
    }

    return allow();
  },
});

// register policies below to apply them
invitationPolicyRegistry
  .registerPolicy(emailValidationPolicy)
  .registerPolicy(maxInvitationsPolicy);
```

`registerPolicy()` returns the registry, so calls can be chained. Registering two policies with the same `id` throws an error.

**About configuration**: `evaluator.evaluate(registry, ...)` calls each policy without a config object, so a configurable policy evaluated through the registry always uses the default in its `evaluate` signature (`{ maxInvitations: 5 }` above). To pass a specific config, load the policy with `createPoliciesFromRegistry()` (see [Passing Configuration](#passing-configuration)).

The invitation context (`FeaturePolicyInvitationContext`, in `feature-policy-invitation-context.ts`) gives policies `accountSlug`, `accountId`, `subscription` (with `status`, `provider`, `active`, trial dates and `items`), `currentMemberCount`, `invitations` (`{ email, role }[]`) and `invitingUser`.

### 2. Create a Feature Policy Evaluator

The kit already ships this evaluator for invitations in `packages/features/team-accounts/src/server/policies/invitation-policies.ts`:

```typescript {% title="packages/features/team-accounts/src/server/policies/invitation-policies.ts" %}
import { createPoliciesEvaluator } from '@kit/policies';

import type { FeaturePolicyInvitationContext } from './feature-policy-invitation-context';
import { invitationPolicyRegistry } from './policies';

export function createInvitationsPolicyEvaluator() {
  const evaluator = createPoliciesEvaluator<FeaturePolicyInvitationContext>();

  return {
    async hasPoliciesForStage(stage: 'preliminary' | 'submission') {
      return evaluator.hasPoliciesForStage(invitationPolicyRegistry, stage);
    },

    async canInvite(
      context: FeaturePolicyInvitationContext,
      stage: 'preliminary' | 'submission',
    ) {
      return evaluator.evaluate(
        invitationPolicyRegistry,
        context,
        'ALL',
        stage,
      );
    },
  };
}
```

Follow the same shape when you add a registry for a new feature.

### 3. Use the Policy Evaluator

This is how the kit evaluates invitation policies inside the `createInvitationsFunction` server function (`packages/features/team-accounts/src/server/functions/team-invitations.functions.ts`):

```typescript
// Excerpt from team-invitations.functions.ts
import { getSupabaseServerClient } from '@kit/supabase/server-client';

import { createInvitationContextBuilder } from '../policies/invitation-context-builder';
import { createInvitationsPolicyEvaluator } from '../policies/invitation-policies';

async function evaluateInvitationsPolicies(
  params: z.output<typeof InviteMembersSchema> & { accountSlug: string },
  user: JWTUserData,
  accountId: string,
) {
  const evaluator = createInvitationsPolicyEvaluator();
  const hasPolicies = await evaluator.hasPoliciesForStage('submission');

  // No policies to evaluate, skip
  if (!hasPolicies) {
    return {
      allowed: true,
      reasons: [],
    };
  }

  const client = getSupabaseServerClient();
  const builder = createInvitationContextBuilder(client);
  const context = await builder.buildContextWithAccountId(
    params,
    user,
    accountId,
  );

  return evaluator.canInvite(context, 'submission');
}
```

When `allowed` is `false`, the server function returns `{ success: false, reasons }` instead of sending the invitations.

## Error Handling

The `deny()` helper supports both simple strings and structured errors.

### String Errors (Simple)

```typescript
return deny('Email validation failed');

// with optional metadata
return deny('Email validation failed', { fieldName: 'email' });
```

### Structured Errors (Enhanced)

```typescript
return deny({
  code: 'INVALID_EMAIL_FORMAT',
  message: 'Email validation failed',
  remediation: 'Please provide a valid email address',
  metadata: { fieldName: 'email' },
});
```

The `message` becomes the result's `reason`; `code`, `remediation` and any extra `metadata` are merged into the result's `metadata`. The invite-members dialog renders the first reason with `Trans`, so you can use an i18n key (like `teams.policyErrors.subscriptionRequired`) as the message.

### Accessing Error Details

```typescript
const result = await evaluator.canInvite(context, 'submission');

if (!result.allowed) {
  console.log('Reasons:', result.reasons);

  result.results.forEach((policyResult) => {
    if (!policyResult.allowed && policyResult.metadata) {
      console.log('Policy:', policyResult.metadata.policyId);
      console.log('Error code:', policyResult.metadata.code);
      console.log('Remediation:', policyResult.metadata.remediation);
    }
  });
}
```

Policies defined with `definePolicy()` add `policyId` and `stage` to each result's metadata. If a policy throws, the error is caught and returned as a denial with the code `POLICY_EVALUATION_ERROR`.

## Performance Optimizations

### 1. Lazy Context Building

Only build expensive context when policies exist. With no policies registered (the kit default), `hasPoliciesForStage()` returns `false` and the database queries in `createInvitationContextBuilder()` never run:

```typescript
const hasPolicies = await evaluator.hasPoliciesForStage('submission');

if (!hasPolicies) {
  return; // Skip expensive operations
}

// Build context now that policies need to run
const context = await builder.buildContextWithAccountId(params, user, accountId);
const result = await evaluator.canInvite(context, 'submission');
```

### 2. Stage-Aware Evaluation

Policies only run for the stages listed in their `stages` array (a policy without `stages` runs in every stage). The kit uses two stages for invitations:

- **`preliminary`** - evaluated when the invite-members dialog opens, by the `GET` handler in `apps/web/src/routes/_authenticated/settings/members/policies.ts`. It builds the context with an empty `invitations` array, so use it for account-level checks (subscription, plan, member count).
- **`submission`** - evaluated by `createInvitationsFunction` with the actual invitations.

```typescript
// Fast preliminary checks (dialog open)
const prelimResult = await evaluator.canInvite(context, 'preliminary');

// Full submission validation
const submitResult = await evaluator.canInvite(context, 'submission');
```

### 3. AND/OR Logic

Control evaluation behavior:

```typescript
const evaluator = createPoliciesEvaluator();

// ALL: Every policy must pass (default)
const allResult = await evaluator.evaluate(registry, context, 'ALL', stage);

// ANY: At least one policy must pass
const anyResult = await evaluator.evaluate(registry, context, 'ANY', stage);
```

With `ANY` and no matching policies, the result is `allowed: false` with the reason `No policies matched the criteria`.

## Passing Configuration

To run registered policies with a specific config, convert them into policy functions with `createPoliciesFromRegistry()` and evaluate them with `evaluatePolicies()`. Each entry is either a policy ID or a `[policyId, config]` tuple; the config is validated against the policy's `configSchema`:

```typescript
import {
  createPoliciesEvaluator,
  createPoliciesFromRegistry,
} from '@kit/policies';

import type { FeaturePolicyInvitationContext } from './feature-policy-invitation-context';
import { invitationPolicyRegistry } from './policies';

const policies =
  await createPoliciesFromRegistry<FeaturePolicyInvitationContext>(
    invitationPolicyRegistry,
    ['email-validation', ['max-invitations', { maxInvitations: 20 }]],
  );

const evaluator = createPoliciesEvaluator<FeaturePolicyInvitationContext>();

const result = await evaluator.evaluatePolicies(
  policies,
  context,
  'ALL',
  'submission',
);
```

## Real-World Example: Team Invitations

The kit's invitation registry file defines the registry and one ready-made policy, `subscriptionRequiredInvitationsPolicy`. **The policy is defined but not registered**, so it does not run until you register it:

```typescript {% title="packages/features/team-accounts/src/server/policies/policies.ts" %}
import { allow, definePolicy, deny } from '@kit/policies';
import { createPolicyRegistry } from '@kit/policies';

import type { FeaturePolicyInvitationContext } from './feature-policy-invitation-context';

/**
 * Feature-specific registry for invitation policies
 */
export const invitationPolicyRegistry = createPolicyRegistry();

/**
 * Subscription required policy
 * Checks if the account has an active subscription
 */
export const subscriptionRequiredInvitationsPolicy =
  definePolicy<FeaturePolicyInvitationContext>({
    id: 'subscription-required',
    stages: ['preliminary', 'submission'],
    evaluate: async ({ subscription }) => {
      if (!subscription || !subscription.active) {
        return deny({
          code: 'SUBSCRIPTION_REQUIRED',
          message: 'teams.policyErrors.subscriptionRequired',
          remediation: 'teams.policyRemediation.subscriptionRequired',
        });
      }

      return allow();
    },
  });

// register policies below to apply them
//
//
```

To require an active subscription before a team can invite members, add this line under the `// register policies below to apply them` comment:

```typescript
invitationPolicyRegistry.registerPolicy(subscriptionRequiredInvitationsPolicy);
```

Because the policy runs in the `preliminary` stage, the invite-members dialog shows the translated `teams.policyErrors.subscriptionRequired` message as soon as it opens, and the `submission` stage blocks the server function as well. Both translation keys already exist in `packages/i18n/src/messages/en/teams.json`.

### Team Account Creation

The same pattern applies to team account creation. The registry in `create-account-policies.server.ts` is empty (`// Register policies here`). Policies receive a `FeaturePolicyCreateAccountContext` with `userId` and `accountName` (an empty string during the `preliminary` check). The example below is one you add yourself:

```typescript {% title="packages/features/team-accounts/src/server/policies/create-account-policies.server.ts" %}
import { allow, createPolicyRegistry, definePolicy, deny } from '@kit/policies';

import type { FeaturePolicyCreateAccountContext } from './feature-policy-create-account-context';

/**
 * Feature-specific registry for create account policies.
 */
export const createAccountPolicyRegistry = createPolicyRegistry();

// Example policy you add yourself
const reservedNamesPolicy = definePolicy<FeaturePolicyCreateAccountContext>({
  id: 'reserved-team-names',
  stages: ['submission'],
  evaluate: async ({ accountName }) => {
    const reserved = ['admin', 'support', 'billing'];

    if (reserved.includes(accountName.trim().toLowerCase())) {
      return deny({
        code: 'RESERVED_TEAM_NAME',
        message: 'This team name is reserved',
        remediation: 'Choose a different team name',
      });
    }

    return allow();
  },
});

// Register policies here
createAccountPolicyRegistry.registerPolicy(reservedNamesPolicy);
```

The `submission` stage runs inside the create-team server function (`create-team-account.functions.ts`); the `preliminary` stage runs in `apps/web/src/lib/server/compose-user-workspace.server.ts` to decide whether the user can create a team at all.

## Customer Extension Pattern

Customers can extend policies by adding to the kit's registries (shown above), creating their own registries, or composing policy evaluators. The examples below are code you add yourself.

### Method 1: Own Registry

```typescript {% title="customer-invitation-policies.ts" %}
import {
  allow,
  createPoliciesEvaluator,
  createPolicyRegistry,
  definePolicy,
  deny,
} from '@kit/policies';
import type { FeaturePolicyInvitationContext } from '@kit/team-accounts/policies';

const customerInvitationRegistry = createPolicyRegistry();

customerInvitationRegistry.registerPolicy(
  definePolicy<FeaturePolicyInvitationContext>({
    id: 'custom-domain-check',
    stages: ['submission'],
    evaluate: async ({ invitations }) => {
      const allowedDomains = ['company.com', 'partner.com'];

      for (const invitation of invitations) {
        const domain = invitation.email.split('@')[1] ?? '';

        if (!allowedDomains.includes(domain)) {
          return deny({
            code: 'DOMAIN_NOT_ALLOWED',
            message: `Email domain ${domain} is not allowed`,
            remediation: 'Use an email from an approved domain',
          });
        }
      }

      return allow();
    },
  }),
);

export function createCustomInvitationPolicyEvaluator() {
  const evaluator = createPoliciesEvaluator<FeaturePolicyInvitationContext>();

  return {
    async validateCustomRules(
      context: FeaturePolicyInvitationContext,
      stage: 'preliminary' | 'submission',
    ) {
      return evaluator.evaluate(
        customerInvitationRegistry,
        context,
        'ALL',
        stage,
      );
    },
  };
}
```

### Method 2: Compose Policy Evaluators

```typescript
// Use both built-in and custom policies
import {
  type FeaturePolicyInvitationContext,
  createInvitationsPolicyEvaluator,
} from '@kit/team-accounts/policies';

import { createCustomInvitationPolicyEvaluator } from './customer-invitation-policies';

async function validateInvitations(
  context: FeaturePolicyInvitationContext,
  stage: 'preliminary' | 'submission',
) {
  const builtinEvaluator = createInvitationsPolicyEvaluator();
  const customEvaluator = createCustomInvitationPolicyEvaluator();

  // Run built-in policies
  const builtinResult = await builtinEvaluator.canInvite(context, stage);

  if (!builtinResult.allowed) {
    throw new Error(builtinResult.reasons.join(', '));
  }

  // Run custom policies
  const customResult = await customEvaluator.validateCustomRules(context, stage);

  if (!customResult.allowed) {
    throw new Error(customResult.reasons.join(', '));
  }
}
```

The kit's server function only calls `createInvitationsPolicyEvaluator()`, so a separate registry runs only where you call it yourself. Adding policies to `invitationPolicyRegistry` in `policies.ts` is the way to have them enforced by the built-in invitation flow.

## Complex Group Evaluation

For advanced scenarios requiring complex business logic with multiple decision paths, use `createPolicy()` and `evaluateGroups()`. The context below is a hypothetical example you would define for your own feature:

### Example: Multi-Stage Enterprise Validation

```typescript
import {
  type PolicyContext,
  type PolicyGroup,
  allow,
  createPoliciesEvaluator,
  createPolicy,
  deny,
} from '@kit/policies';

// Example context you define yourself
interface FeatureContext extends PolicyContext {
  userId?: string;
  email?: string;
  permissions: string[];
  role: string;
  subscription?: { plan: string; active: boolean };
  trial?: { type: string; daysRemaining: number };
  adminOverride?: { enabled: boolean };
  memberCount: number;
  maxMembers: number;
  complianceStatus: 'approved' | 'pending';
}

// Complex scenario: (Authentication AND Email) AND (Subscription OR Trial) AND Final Validation
async function validateEnterpriseFeatureAccess(context: FeatureContext) {
  const evaluator = createPoliciesEvaluator<FeatureContext>();

  // Stage 1: Authentication Requirements (ALL must pass)
  const authenticationGroup: PolicyGroup<FeatureContext> = {
    operator: 'ALL',
    policies: [
      createPolicy<FeatureContext>(async (ctx) =>
        ctx.userId
          ? allow({ step: 'authenticated' })
          : deny('Authentication required'),
      ),
      createPolicy<FeatureContext>(async (ctx) =>
        ctx.email?.includes('@')
          ? allow({ step: 'email-valid' })
          : deny('Valid email required'),
      ),
      createPolicy<FeatureContext>(async (ctx) =>
        ctx.permissions.includes('enterprise-features')
          ? allow({ step: 'permissions' })
          : deny('Enterprise permissions required'),
      ),
    ],
  };

  // Stage 2: Billing Validation (ANY sufficient - flexible payment options)
  const billingGroup: PolicyGroup<FeatureContext> = {
    operator: 'ANY',
    policies: [
      createPolicy<FeatureContext>(async (ctx) =>
        ctx.subscription?.plan === 'enterprise' && ctx.subscription.active
          ? allow({ billing: 'enterprise-subscription' })
          : deny('Enterprise subscription required'),
      ),
      createPolicy<FeatureContext>(async (ctx) =>
        ctx.trial?.type === 'enterprise' && ctx.trial.daysRemaining > 0
          ? allow({
              billing: 'enterprise-trial',
              daysLeft: ctx.trial.daysRemaining,
            })
          : deny('Active enterprise trial required'),
      ),
      createPolicy<FeatureContext>(async (ctx) =>
        ctx.adminOverride?.enabled && ctx.role === 'super-admin'
          ? allow({ billing: 'admin-override' })
          : deny('Admin override not available'),
      ),
    ],
  };

  // Stage 3: Final Constraints (ALL must pass)
  const constraintsGroup: PolicyGroup<FeatureContext> = {
    operator: 'ALL',
    policies: [
      createPolicy<FeatureContext>(async (ctx) =>
        ctx.memberCount <= ctx.maxMembers
          ? allow({ constraint: 'team-size-valid' })
          : deny('Team size exceeds plan limits'),
      ),
      createPolicy<FeatureContext>(async (ctx) =>
        ctx.complianceStatus === 'approved'
          ? allow({ constraint: 'compliance-approved' })
          : deny('Organization compliance approval required'),
      ),
    ],
  };

  // Execute all groups sequentially - ALL groups must pass
  const result = await evaluator.evaluateGroups(
    [authenticationGroup, billingGroup, constraintsGroup],
    context,
  );

  return {
    allowed: result.allowed,
    reasons: result.reasons,
    metadata: {
      authenticationPassed: result.results.some(
        (r) => r.metadata?.step === 'authenticated',
      ),
      billingMethod: result.results.find((r) => r.metadata?.billing)?.metadata
        ?.billing,
      constraintsValidated: result.results.some((r) => r.metadata?.constraint),
    },
  };
}
```

### Group Evaluation Flow

1. **Sequential Group Processing**: Groups are evaluated in order
2. **All Groups Must Pass**: If any group fails, entire evaluation fails
3. **Short-Circuiting**: Stops on first group failure for performance
4. **Metadata Preservation**: All policy results and metadata are collected

`evaluateGroups(groups, context, stage?)` also accepts an optional stage, which is passed to each policy function as its second argument.

### Group Operators

- **`ALL` (AND logic)**: All policies in the group must pass
  - Short-circuits on first failure for performance
  - Use for mandatory requirements where every condition must be met

- **`ANY` (OR logic)**: At least one policy in the group must pass
  - Short-circuits on first success for performance
  - Use for flexible requirements where multiple options are acceptable

### Performance Considerations

- **Order groups by criticality**: Put fast, critical checks first
- **Group by evaluation cost**: Separate expensive operations
- **Monitor evaluation time**: Track performance for optimization

## API Reference

All exports come from `@kit/policies` (`packages/policies/src/index.ts`).

### Core Functions

- `createPolicyRegistry()` - Create a feature-specific registry
- `definePolicy({ id, stages?, configSchema?, evaluate })` - Define a policy with metadata and optional configuration
- `createPoliciesEvaluator<TContext>(options?)` - Create a policy evaluator instance (`options.maxCacheSize` defaults to 100)
- `createPolicy(evaluate)` - Create a plain policy function for groups
- `createPolicyFromRegistry(registry, policyId, config?)` - Convert a registered policy into a policy function
- `createPoliciesFromRegistry(registry, specs)` - Convert several registered policies; each spec is an ID or a `[id, config]` tuple
- `allow(metadata?)` - Return a success result with optional metadata
- `deny(reason, metadata?)` or `deny({ code, message, remediation?, metadata? })` - Return a failure result

### Policy Registry Methods

- `registry.registerPolicy(definition)` - Register a policy (chainable; throws on duplicate IDs)
- `registry.getPolicy(id)` - Get a policy definition (throws if not registered)
- `registry.hasPolicy(id)` - Check if a policy is registered
- `registry.listPolicies()` - List registered policy IDs in registration order

### Policy Evaluator Methods

- `evaluator.evaluate(registry, context, operator?, stage?)` - Evaluate registry policies (`operator` defaults to `'ALL'`)
- `evaluator.evaluatePolicies(policies, context, operator?, stage?)` - Evaluate an array of policy functions
- `evaluator.evaluateGroup(group, context, stage?)` - Evaluate a single group
- `evaluator.evaluateGroups(groups, context, stage?)` - Evaluate complex group logic
- `evaluator.hasPoliciesForStage(registry, stage?)` - Check if policies exist for a stage
- `evaluator.clearCache()` - Clear the evaluator's cached policy definitions

### Types

- `PolicyContext` - Base context interface (`timestamp`, optional `metadata`)
- `PolicyResult` - Policy evaluation result (`allowed`, `reason?`, `requiresManualReview?`, `metadata?`)
- `PolicyStage` - Execution stage (any `string`, such as `'preliminary'` or `'submission'`)
- `EvaluationResult` - Contains `allowed`, `reasons`, and `results` arrays
- `PolicyGroup` - Group configuration with `operator` and `policies`
- `PolicyFunction` - A policy function `(context, stage?) => Promise<PolicyResult>`
- `PolicyRegistry` - The registry interface
- `FeaturePolicyDefinition` - The object returned by `definePolicy()`
- `PolicyErrorCode` - Structured error accepted by `deny()`

## Related documentation

- [Team Account API](/docs/tanstack-supabase/api/team-account-api) - Team management operations
- [Billing Configuration](/docs/tanstack-supabase/billing/overview) - Payment provider setup
- [Row Level Security](/docs/tanstack-supabase/security/row-level-security) - Database-level security
- [Per-seat Billing](/docs/tanstack-supabase/billing/per-seat-billing) - Team-based pricing with seat limits
