# FeaturePolicy API - Registry-Based Policy System A unified, registry-based foundation for implementing business rules across all Makerkit features. ## Overview The FeaturePolicy API provides: - **Registry-based architecture** - centralized policy management with IDs - **Configuration support** - policies can accept typed configuration objects - **Stage-aware evaluation** - policies can be filtered by execution stage - **Immutable contexts** for safe policy evaluation - **Customer extensibility** - easy to add custom policies without forking ## Quick Start ### 1. Register Policies ```typescript import * as z from 'zod'; import { allow, createPolicyRegistry, definePolicy, deny } from '@kit/policies'; const registry = createPolicyRegistry(); // Register a basic policy registry.registerPolicy( definePolicy({ id: 'email-validation', stages: ['preliminary', 'submission'], evaluate: async (context) => { if (!context.userEmail?.includes('@')) { return deny({ code: 'INVALID_EMAIL_FORMAT', message: 'Invalid email format', remediation: 'Please provide a valid email address', }); } return allow(); }, }), ); // Register a configurable policy registry.registerPolicy( definePolicy({ id: 'max-invitations', 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. Use Policies from Registry ```typescript import { createPoliciesFromRegistry, createPolicyEvaluator, createPolicyRegistry, } from '@kit/policies'; const registry = createPolicyRegistry(); // Load policies from registry const policies = await createPoliciesFromRegistry(registry, [ 'email-validation', 'subscription-required', ['max-invitations', { maxInvitations: 5 }], // with configuration ]); const evaluator = createPolicyEvaluator(); const result = await evaluator.evaluatePolicies(policies, context, 'ALL'); if (!result.allowed) { console.log('Failed reasons:', result.reasons); } ``` ### 3. Group Policies with Complex Logic ```typescript // Basic group example const preliminaryGroup = { operator: 'ALL' as const, policies: [emailValidationPolicy, authenticationPolicy], }; const billingGroup = { operator: 'ANY' as const, policies: [subscriptionPolicy, trialPolicy], }; // Evaluate groups in sequence const result = await evaluator.evaluateGroups( [preliminaryGroup, billingGroup], context, ); ``` ## Complex Group Flows ### Real-World Multi-Stage Team Invitation Flow ```typescript import { createPolicy, createPolicyEvaluator } from '@kit/policies'; // Complex business logic: (Authentication AND Email Validation) AND (Subscription OR Trial) AND Billing Limits async function validateTeamInvitation(context: InvitationContext) { const evaluator = createPolicyEvaluator(); // 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('invite') ? allow({ step: 'permissions' }) : deny('Insufficient permissions'), ), ], }; // Stage 2: Subscription Validation (ANY sufficient - flexible billing) const subscriptionGroup = { operator: 'ANY' as const, policies: [ createPolicy(async (ctx) => ctx.subscription?.active && ctx.subscription.plan === 'enterprise' ? allow({ billing: 'enterprise' }) : deny('Enterprise subscription required'), ), createPolicy(async (ctx) => ctx.subscription?.active && ctx.subscription.plan === 'pro' ? allow({ billing: 'pro' }) : deny('Pro subscription required'), ), createPolicy(async (ctx) => ctx.trial?.active && ctx.trial.daysRemaining > 0 ? allow({ billing: 'trial', daysLeft: ctx.trial.daysRemaining }) : deny('Active trial required'), ), ], }; // Stage 3: Final Constraints (ALL must pass) const constraintsGroup = { operator: 'ALL' as const, policies: [ createPolicy(async (ctx) => ctx.team.memberCount < ctx.subscription?.maxMembers ? allow({ constraint: 'member-limit' }) : deny('Member limit exceeded'), ), createPolicy(async (ctx) => ctx.invitations.length <= 10 ? allow({ constraint: 'batch-size' }) : deny('Cannot invite more than 10 members at once'), ), ], }; // Execute all groups sequentially - ALL groups must pass const result = await evaluator.evaluateGroups( [authenticationGroup, subscriptionGroup, constraintsGroup], context, ); return { allowed: result.allowed, reasons: result.reasons, metadata: { stagesCompleted: result.results.length, authenticationPassed: result.results.some( (r) => r.metadata?.step === 'authenticated', ), billingType: result.results.find((r) => r.metadata?.billing)?.metadata ?.billing, constraintsChecked: result.results.some((r) => r.metadata?.constraint), }, }; } ``` ### Middleware-Style Policy Chain ```typescript // Simulate middleware pattern: Auth → Rate Limiting → Business Logic async function processApiRequest(context: ApiContext) { const evaluator = createPoliciesEvaluator(); // Layer 1: Security (ALL required) const securityLayer = { operator: 'ALL' as const, policies: [ createPolicy(async (ctx) => ctx.apiKey && ctx.apiKey.length > 0 ? allow({ security: 'api-key-valid' }) : deny('API key required'), ), createPolicy(async (ctx) => ctx.rateLimitRemaining > 0 ? allow({ security: 'rate-limit-ok' }) : deny('Rate limit exceeded'), ), createPolicy(async (ctx) => !ctx.blacklisted ? allow({ security: 'not-blacklisted' }) : deny('Client is blacklisted'), ), ], }; // Layer 2: Authorization (ANY sufficient - flexible access levels) const authorizationLayer = { operator: 'ANY' as const, policies: [ createPolicy(async (ctx) => ctx.user.role === 'admin' ? allow({ access: 'admin' }) : deny('Admin access denied'), ), createPolicy(async (ctx) => ctx.user.permissions.includes(ctx.requestedResource) ? allow({ access: 'resource-specific' }) : deny('Resource access denied'), ), createPolicy(async (ctx) => ctx.user.subscription?.includes('api-access') ? allow({ access: 'subscription-based' }) : deny('Subscription access denied'), ), ], }; // Layer 3: Business Rules (ALL required) const businessLayer = { operator: 'ALL' as const, policies: [ createPolicy(async (ctx) => ctx.request.size <= ctx.maxRequestSize ? allow({ business: 'size-valid' }) : deny('Request too large'), ), createPolicy(async (ctx) => ctx.user.dailyQuota > ctx.user.dailyUsage ? allow({ business: 'quota-available' }) : deny('Daily quota exceeded'), ), ], }; return evaluator.evaluateGroups( [securityLayer, authorizationLayer, businessLayer], context, ); } ``` ### Complex Nested Logic with Short-Circuiting ```typescript // Complex scenario: (Premium User OR (Basic User AND Low Usage)) AND Security Checks async function validateFeatureAccess(context: FeatureContext) { const evaluator = createPoliciesEvaluator(); // Group 1: User Tier Logic - demonstrates complex OR conditions const userTierGroup = { operator: 'ANY' as const, policies: [ // Premium users get immediate access createPolicy(async (ctx) => ctx.user.plan === 'premium' ? allow({ tier: 'premium', reason: 'premium-user' }) : deny('Not premium user'), ), // Enterprise users get immediate access createPolicy(async (ctx) => ctx.user.plan === 'enterprise' ? allow({ tier: 'enterprise', reason: 'enterprise-user' }) : deny('Not enterprise user'), ), // Basic users need additional validation (sub-group logic) createPolicy(async (ctx) => { if (ctx.user.plan !== 'basic') { return deny('Not basic user'); } // Simulate nested AND logic for basic users const basicUserRequirements = [ ctx.user.monthlyUsage < 1000, ctx.user.accountAge > 30, // days !ctx.user.hasViolations, ]; const allBasicRequirementsMet = basicUserRequirements.every( (req) => req, ); return allBasicRequirementsMet ? allow({ tier: 'basic', reason: 'low-usage-basic-user' }) : deny('Basic user requirements not met'); }), ], }; // Group 2: Security Requirements (ALL must pass) const securityGroup = { operator: 'ALL' as const, policies: [ createPolicy(async (ctx) => ctx.user.emailVerified ? allow({ security: 'email-verified' }) : deny('Email verification required'), ), createPolicy(async (ctx) => ctx.user.twoFactorEnabled || ctx.user.plan === 'basic' ? allow({ security: '2fa-compliant' }) : deny('Two-factor authentication required for premium plans'), ), createPolicy(async (ctx) => !ctx.user.suspiciousActivity ? allow({ security: 'activity-clean' }) : deny('Suspicious activity detected'), ), ], }; return evaluator.evaluateGroups([userTierGroup, securityGroup], context); } ``` ### Dynamic Policy Composition ```typescript // Dynamically compose policies based on context async function createContextAwarePolicyFlow(context: DynamicContext) { const evaluator = createPoliciesEvaluator(); const groups = []; // Always include base security const baseSecurityGroup = { operator: 'ALL' as const, policies: [ createPolicy(async (ctx) => ctx.isAuthenticated ? allow() : deny('Authentication required'), ), ], }; groups.push(baseSecurityGroup); // Add user-type specific policies if (context.user.type === 'admin') { const adminGroup = { operator: 'ALL' as const, policies: [ createPolicy(async (ctx) => ctx.user.adminLevel >= ctx.requiredAdminLevel ? allow({ admin: 'level-sufficient' }) : deny('Insufficient admin level'), ), createPolicy(async (ctx) => ctx.user.lastLogin > Date.now() - 24 * 60 * 60 * 1000 // 24 hours ? allow({ admin: 'recent-login' }) : deny('Admin must have logged in within 24 hours'), ), ], }; groups.push(adminGroup); } // Add feature-specific policies based on requested feature if (context.feature.requiresBilling) { const billingGroup = { operator: 'ANY' as const, policies: [ createPolicy(async (ctx) => ctx.subscription?.active ? allow({ billing: 'subscription' }) : deny('Active subscription required'), ), createPolicy(async (ctx) => ctx.credits && ctx.credits > ctx.feature.creditCost ? allow({ billing: 'credits' }) : deny('Insufficient credits'), ), ], }; groups.push(billingGroup); } // Add rate limiting for high-impact features if (context.feature.highImpact) { const rateLimitGroup = { operator: 'ALL' as const, policies: [ createPolicy(async (ctx) => ctx.rateLimit.remaining > 0 ? allow({ rateLimit: 'within-limits' }) : deny('Rate limit exceeded for high-impact features'), ), ], }; groups.push(rateLimitGroup); } return evaluator.evaluateGroups(groups, context); } ``` ### Performance-Optimized Large Group Evaluation ```typescript // Handle large numbers of policies efficiently async function validateComplexBusinessRules(context: BusinessContext) { const evaluator = createPoliciesEvaluator({ maxCacheSize: 200 }); // Group policies by evaluation cost and criticality const criticalFastGroup = { operator: 'ALL' as const, policies: [ // Fast critical checks first createPolicy(async (ctx) => ctx.isActive ? allow() : deny('Account inactive'), ), createPolicy(async (ctx) => ctx.hasPermission ? allow() : deny('No permission'), ), createPolicy(async (ctx) => !ctx.isBlocked ? allow() : deny('Account blocked'), ), ], }; const businessLogicGroup = { operator: 'ANY' as const, policies: [ // Complex business rules createPolicy(async (ctx) => { // Simulate complex calculation const score = await calculateRiskScore(ctx); return score < 0.8 ? allow({ risk: 'low' }) : deny('High risk detected'); }), createPolicy(async (ctx) => { // Simulate external API call const verification = await verifyWithThirdParty(ctx); return verification.success ? allow({ external: 'verified' }) : deny('External verification failed'); }), ], }; const finalValidationGroup = { operator: 'ALL' as const, policies: [ // Final checks after complex logic createPolicy(async (ctx) => ctx.complianceCheck ? allow() : deny('Compliance check failed'), ), ], }; // Use staged evaluation for better performance const startTime = Date.now(); const result = await evaluator.evaluateGroups( [ criticalFastGroup, // Fast critical checks first businessLogicGroup, // Complex logic only if critical checks pass finalValidationGroup, // Final validation ], context, ); const evaluationTime = Date.now() - startTime; return { ...result, performance: { evaluationTimeMs: evaluationTime, groupsEvaluated: result.results.length > 0 ? 3 : 1, }, }; } // Helper functions for complex examples async function calculateRiskScore(context: any): Promise { // Simulate complex risk calculation await new Promise((resolve) => setTimeout(resolve, 10)); return Math.random(); } async function verifyWithThirdParty( context: any, ): Promise<{ success: boolean }> { // Simulate external API call await new Promise((resolve) => setTimeout(resolve, 5)); return { success: Math.random() > 0.2 }; } ``` ## Advanced Usage ### Configurable Policies ```typescript // Create policy factories for configuration const createMaxInvitationsPolicy = (maxInvitations: number) => createPolicy(async (context) => { if (context.invitations.length > maxInvitations) { return deny({ code: 'MAX_INVITATIONS_EXCEEDED', message: `Cannot invite more than ${maxInvitations} members`, remediation: `Reduce invitations to ${maxInvitations} or fewer`, }); } return allow(); }); // Use with different configurations const strictPolicy = createMaxInvitationsPolicy(1); const standardPolicy = createMaxInvitationsPolicy(5); const permissivePolicy = createMaxInvitationsPolicy(25); ``` ### Feature-Specific evaluators ```typescript // Create feature-specific evaluator with preset configurations export function createInvitationevaluator( preset: 'strict' | 'standard' | 'permissive', ) { const configs = { strict: { maxInvitationsPerBatch: 1 }, standard: { maxInvitationsPerBatch: 5 }, permissive: { maxInvitationsPerBatch: 25 }, }; const config = configs[preset]; return { async validateInvitations(context: InvitationContext) { const policies = [ emailValidationPolicy, createMaxInvitationsPolicy(config.maxInvitationsPerBatch), subscriptionRequiredPolicy, paddleBillingPolicy, ]; const evaluator = createPoliciesEvaluator(); return evaluator.evaluatePolicies(policies, context, 'ALL'); }, }; } // Usage const evaluator = createInvitationevaluator('standard'); const result = await evaluator.validateInvitations(context); ``` ### Error Handling ```typescript const result = await evaluator.evaluate(); if (!result.allowed) { result.reasons.forEach((reason) => { console.log(`Policy ${reason.policyId} failed:`); console.log(` Code: ${reason.code}`); console.log(` Message: ${reason.message}`); if (reason.remediation) { console.log(` Fix: ${reason.remediation}`); } }); } ``` ### 1. Register Complex Policy with Configuration ```typescript import { createPolicyRegistry, definePolicy } from '@kit/policies'; const registry = createPolicyRegistry(); const customConfigurablePolicy = definePolicy({ id: 'custom-domain-check', configSchema: z.object({ allowedDomains: z.array(z.string()), strictMode: z.boolean(), }), evaluate: async (context, config) => { const emailDomain = context.userEmail?.split('@')[1]; if (config?.strictMode && !config.allowedDomains.includes(emailDomain)) { return deny({ code: 'DOMAIN_NOT_ALLOWED', message: `Email domain ${emailDomain} is not in the allowed list`, remediation: 'Use an email from an approved domain', }); } return allow(); }, }); registry.registerPolicy(customConfigurablePolicy); ``` ## Key Concepts ### 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 - Example: Authentication AND permissions AND rate limiting - **`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 - Example: Premium subscription OR trial access OR admin override ### 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 ### Performance Considerations - **Order groups by criticality**: Put fast, critical checks first - **Use caching**: Configure `maxCacheSize` for frequently used policies - **Group by evaluation cost**: Separate expensive operations - **Monitor evaluation time**: Track performance for optimization ## Stage-Aware Evaluation Policies can be filtered by execution stage. This is useful for running a subset of policies depending on the situation: ```typescript // Only run preliminary checks const prelimResult = await evaluator.evaluate( registry, context, 'ALL', 'preliminary', ); // Run submission validation const submitResult = await evaluator.evaluate( registry, context, 'ALL', 'submission', ); // Run all applicable policies const fullResult = await evaluator.evaluate(registry, context, 'ALL'); ```