• Blog
  • Documentation
  • Courses
  • Changelog
  • AI Starters
  • UI Kit
  • FAQ
  • Supamode
    New
  • Pricing

Launch your next SaaS in record time with Makerkit, a React SaaS Boilerplate for Next.js and Supabase.

Makerkit is a product of Makerkit Pte Ltd (registered in the Republic of Singapore)Company Registration No: 202407149CFor support or inquiries, please contact us

About
  • FAQ
  • Contact
  • Verify your Discord
  • Consultation
  • Open Source
  • Become an Affiliate
Product
  • Documentation
  • Blog
  • Changelog
  • UI Blocks
  • Figma UI Kit
  • AI SaaS Starters
License
  • Activate License
  • Upgrade License
  • Invite Member
Legal
  • Terms of License
    • Account API
    • Team Account API
    • Authentication API
    • User Workspace API
    • Team Workspace API
    • OTP API
    • Registry API
    • Feature Policies API

Feature Policy API - Declarative Policies for complex

A unified, registry-based foundation for implementing business rules across all Makerkit features.

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 currently uses the Feature Policy API for team invitation flows to validate when a team can send 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 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

1. Create a Feature-Specific Registry

import {
createPolicyRegistry,
definePolicy,
createPoliciesEvaluator,
allow,
deny,
} from '@kit/policies';
// Create feature-specific registry
const invitationPolicyRegistry = createPolicyRegistry();
// Register policies
invitationPolicyRegistry.registerPolicy(
definePolicy({
id: 'email-validation',
stages: ['preliminary', 'submission'],
evaluate: async (context) => {
if (!context.invitations.some((inv) => inv.email?.includes('@'))) {
return deny({
code: 'INVALID_EMAIL_FORMAT',
message: 'Invalid email format',
remediation: 'Please provide a valid email address',
});
}
return allow();
},
}),
);
// Register configurable policy
invitationPolicyRegistry.registerPolicy(
definePolicy({
id: 'max-invitations',
stages: ['preliminary', 'submission'],
configSchema: z.object({
maxInvitations: z.number().positive(),
}),
evaluate: async (context, config = { maxInvitations: 5 }) => {
if (context.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();
},
}),
);

2. Create a Feature Policy Evaluator

export function createInvitationsPolicyEvaluator() {
const evaluator = createPoliciesEvaluator();
return {
async hasPoliciesForStage(stage: 'preliminary' | 'submission') {
return evaluator.hasPoliciesForStage(invitationPolicyRegistry, stage);
},
async canInvite(context, stage: 'preliminary' | 'submission') {
return evaluator.evaluate(invitationPolicyRegistry, context, 'ALL', stage);
},
};
}

3. Use the Policy Evaluator

import { createInvitationsPolicyEvaluator } from './your-policies';
async function validateInvitations(context) {
const evaluator = createInvitationsPolicyEvaluator();
// Performance optimization: only build context if policies exist
const hasPolicies = await evaluator.hasPoliciesForStage('submission');
if (!hasPolicies) {
return; // No policies to evaluate
}
const result = await evaluator.canInvite(context, 'submission');
if (!result.allowed) {
throw new Error(result.reasons.join(', '));
}
}

Error Handling

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

String Errors (Simple)

return deny('Email validation failed');

Structured Errors (Enhanced)

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

Accessing Error Details

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('Error code:', policyResult.metadata.code);
console.log('Remediation:', policyResult.metadata.remediation);
}
});
}

Performance Optimizations

1. Lazy Context Building

Only build expensive context when policies exist:

const hasPolicies = await evaluator.hasPoliciesForStage('submission');
if (!hasPolicies) {
return; // Skip expensive operations
}
// Build context now that policies need to run
const context = await buildExpensiveContext();
const result = await evaluator.canInvite(context, 'submission');

2. Stage-Aware Evaluation

Filter policies by execution stage:

// Fast preliminary checks
const prelimResult = await evaluator.canInvite(context, 'preliminary');
// Full submission validation
const submitResult = await evaluator.canInvite(context, 'submission');

3. AND/OR Logic

Control evaluation behavior:

// ALL: Every policy must pass (default)
const result = await evaluator.evaluate(registry, context, 'ALL', stage);
// ANY: At least one policy must pass
const result = await evaluator.evaluate(registry, context, 'ANY', stage);

Real-World Example: Team Invitations

Makerkit uses the Feature Policy API to power team invitation rules.

// packages/features/team-accounts/src/server/policies/invitation-policies.ts
import { allow, definePolicy, deny } from '@kit/policies';
import { createPolicyRegistry } from '@kit/policies';
import { 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();
},
});
/**
* Paddle billing policy
* Checks if the account has a paddle subscription and is in a trial period
*/
export const paddleBillingInvitationsPolicy =
definePolicy<FeaturePolicyInvitationContext>({
id: 'paddle-billing',
stages: ['preliminary', 'submission'],
evaluate: async ({ subscription }) => {
// combine with subscriptionRequiredPolicy if subscription must be required
if (!subscription) {
return allow();
}
// Paddle specific constraint: cannot update subscription items during trial
if (
subscription.provider === 'paddle' &&
subscription.status === 'trialing'
) {
const hasPerSeatItems = subscription.items.some(
(item) => item.type === 'per_seat',
);
if (hasPerSeatItems) {
return deny({
code: 'PADDLE_TRIAL_RESTRICTION',
message: 'teams:policyErrors.paddleTrialRestriction',
remediation: 'teams:policyRemediation.paddleTrialRestriction',
});
}
}
return allow();
},
});
// Register policies to apply them
invitationPolicyRegistry.registerPolicy(subscriptionRequiredInvitationsPolicy);
invitationPolicyRegistry.registerPolicy(paddleBillingInvitationsPolicy);
export function createInvitationsPolicyEvaluator() {
const evaluator = createPoliciesEvaluator();
return {
async hasPoliciesForStage(stage: 'preliminary' | 'submission') {
return evaluator.hasPoliciesForStage(invitationPolicyRegistry, stage);
},
async canInvite(context, stage: 'preliminary' | 'submission') {
return evaluator.evaluate(invitationPolicyRegistry, context, 'ALL', stage);
},
};
}

Customer Extension Pattern

Customers can extend policies by creating their own registries, adding to existing registries, or composing policy evaluators.

Method 1: Own Registry

// customer-invitation-policies.ts
import { createPolicyRegistry, definePolicy } from '@kit/policies';
const customerInvitationRegistry = createPolicyRegistry();
customerInvitationRegistry.registerPolicy(
definePolicy({
id: 'custom-domain-check',
stages: ['preliminary'],
evaluate: async (context) => {
const allowedDomains = ['company.com', 'partner.com'];
for (const invitation of context.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();
return {
async validateCustomRules(context, stage) {
return evaluator.evaluate(customerInvitationRegistry, context, 'ALL', stage);
},
};
}

Method 2: Compose Policy Evaluators

// Use both built-in and custom policies
import { createInvitationsPolicyEvaluator } from '@kit/team-accounts/policies';
import { createCustomInvitationPolicyEvaluator } from './customer-policies';
async function validateInvitations(context, stage) {
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(', '));
}
}

Complex Group Evaluation

For advanced scenarios requiring complex business logic with multiple decision paths:

Example: Multi-Stage Enterprise Validation

// Complex scenario: (Authentication AND Email) AND (Subscription OR Trial) AND Final Validation
async function validateEnterpriseFeatureAccess(context: FeatureContext) {
const evaluator = createPoliciesEvaluator();
// Stage 1: Authentication Requirements (ALL must pass)
const authenticationGroup = {
operator: 'ALL' as const,
policies: [
createPolicy(async (ctx) =>
ctx.userId ? allow({ step: 'authenticated' }) : deny('Authentication required')
),
createPolicy(async (ctx) =>
ctx.email?.includes('@') ? allow({ step: 'email-valid' }) : deny('Valid email required')
),
createPolicy(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 = {
operator: 'ANY' as const,
policies: [
createPolicy(async (ctx) =>
ctx.subscription?.plan === 'enterprise' && ctx.subscription.active
? allow({ billing: 'enterprise-subscription' })
: deny('Enterprise subscription required')
),
createPolicy(async (ctx) =>
ctx.trial?.type === 'enterprise' && ctx.trial.daysRemaining > 0
? allow({ billing: 'enterprise-trial', daysLeft: ctx.trial.daysRemaining })
: deny('Active enterprise trial required')
),
createPolicy(async (ctx) =>
ctx.adminOverride?.enabled && ctx.user.role === 'super-admin'
? allow({ billing: 'admin-override' })
: deny('Admin override not available')
),
],
};
// Stage 3: Final Constraints (ALL must pass)
const constraintsGroup = {
operator: 'ALL' as const,
policies: [
createPolicy(async (ctx) =>
ctx.team.memberCount <= ctx.maxMembers
? allow({ constraint: 'team-size-valid' })
: deny('Team size exceeds plan limits')
),
createPolicy(async (ctx) =>
ctx.organization.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

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

Core Functions

  • createPolicyRegistry() — Create a feature-specific registry
  • definePolicy(config) — Define a policy with metadata and configuration
  • createPoliciesEvaluator() — Create a policy evaluator instance
  • allow(metadata?) — Return a success result with optional metadata
  • deny(reason | error) — Return a failure result (supports strings and structured errors)

Policy Evaluator Methods

  • evaluator.evaluate(registry, context, operator, stage?) — Evaluate registry policies
  • evaluator.evaluateGroups(groups, context) — Evaluate complex group logic
  • evaluator.hasPoliciesForStage(registry, stage?) — Check if policies exist for a stage

Types

  • PolicyContext — Base context interface
  • PolicyResult — Policy evaluation result
  • PolicyStage — Execution stage ('preliminary' | 'submission' | string)
  • EvaluationResult — Contains allowed, reasons, and results arrays
  • PolicyGroup — Group configuration with operator and policies
On this page
  1. What It's For
    1. What It's NOT For
      1. Key Benefits
        1. How We Use It Today
          1. Why Feature Policies?
            1. Overview
              1. Quick Start
                1. 1. Create a Feature-Specific Registry
                2. 2. Create a Feature Policy Evaluator
                3. 3. Use the Policy Evaluator
              2. Error Handling
                1. String Errors (Simple)
                2. Structured Errors (Enhanced)
                3. Accessing Error Details
              3. Performance Optimizations
                1. 1. Lazy Context Building
                2. 2. Stage-Aware Evaluation
                3. 3. AND/OR Logic
              4. Real-World Example: Team Invitations
                1. Customer Extension Pattern
                  1. Method 1: Own Registry
                  2. Method 2: Compose Policy Evaluators
                2. Complex Group Evaluation
                  1. Example: Multi-Stage Enterprise Validation
                  2. Group Evaluation Flow
                  3. Group Operators
                  4. Performance Considerations
                3. API Reference
                  1. Core Functions
                  2. Policy Evaluator Methods
                  3. Types