Skip to main content

Guards Module

The guards module (template/lib/guards/) provides a plan-based feature access control system. It defines which subscription plans can access which features, enforces numeric limits per plan, and provides both server-side guard functions and a React-friendly result builder for client-side usage.

Architecture Overview

Source Files

FileDescription
lib/guards/index.tsBarrel exports
lib/guards/plan-features.guard.tsComplete guard implementation

Plan Hierarchy

Plans are ordered by level, where higher levels include all features of lower levels when configured with minPlan:

const PLAN_LEVELS: Record<string, number> = {
free: 1,
standard: 2,
premium: 3,
};

getPlanLevel(plan: string): number

Returns the numeric level for a plan string. Returns 0 for unknown plans.

planMeetsRequirement(userPlan: string, requiredPlan: string): boolean

Checks if a user's plan level is greater than or equal to the required plan level.

planMeetsRequirement('premium', 'standard'); // true
planMeetsRequirement('free', 'standard'); // false

Feature Definitions

All available features are declared as constants:

const FEATURES = {
// Submission Features
SUBMIT_PRODUCT: 'submit_product',
EXTENDED_DESCRIPTION: 'extended_description',
UNLIMITED_DESCRIPTION: 'unlimited_description',
UPLOAD_IMAGES: 'upload_images',
UPLOAD_VIDEO: 'upload_video',
VERIFIED_BADGE: 'verified_badge',
SPONSORED_BADGE: 'sponsored_badge',

// Review & Priority
PRIORITY_REVIEW: 'priority_review',
INSTANT_REVIEW: 'instant_review',

// Visibility & Placement
SEARCH_VISIBILITY: 'search_visibility',
CATEGORY_PLACEMENT: 'category_placement',
SPONSORED_POSITION: 'sponsored_position',
HOMEPAGE_FEATURED: 'homepage_featured',
NEWSLETTER_MENTION: 'newsletter_mention',

// Statistics & Analytics
VIEW_STATISTICS: 'view_statistics',
ADVANCED_ANALYTICS: 'advanced_analytics',

// Support
EMAIL_SUPPORT: 'email_support',
PRIORITY_EMAIL_SUPPORT: 'priority_email_support',
PHONE_SUPPORT: 'phone_support',

// Social & Marketing
SOCIAL_SHARING: 'social_sharing',
LEARN_MORE_BUTTON: 'learn_more_button',

// Modifications
FREE_MODIFICATIONS: 'free_modifications',

// Submissions
UNLIMITED_SUBMISSIONS: 'unlimited_submissions',
} as const;

type Feature = (typeof FEATURES)[keyof typeof FEATURES];

Feature Access Matrix

The FEATURE_ACCESS record maps each feature to its access rule:

type FeatureAccess =
| PaymentPlan // Only that specific plan
| PaymentPlan[] // Any of these plans
| 'all' // All plans have access
| { minPlan: PaymentPlan }; // That plan and above

Access Rules Summary

FeatureAccess Rule
submit_product'all'
extended_description{ minPlan: 'standard' }
unlimited_description'premium'
upload_images'all'
upload_video'premium'
verified_badge{ minPlan: 'standard' }
sponsored_badge'premium'
priority_review{ minPlan: 'standard' }
instant_review'premium'
search_visibility'all'
category_placement'all'
sponsored_position'premium'
homepage_featured'premium'
newsletter_mention'premium'
view_statistics{ minPlan: 'standard' }
advanced_analytics'premium'
email_support'all'
priority_email_support{ minPlan: 'standard' }
phone_support'premium'
social_sharing{ minPlan: 'standard' }
learn_more_button'premium'
free_modifications{ minPlan: 'standard' }
unlimited_submissions'premium'

Plan Limits

Numeric limits for features with quantity restrictions. null indicates unlimited.

const PLAN_LIMITS: Record<PaymentPlan, FeatureLimits> = {
free: {
max_images: 1,
max_description_words: 200,
max_submissions: 1,
review_days: 7,
free_modification_days: 0,
},
standard: {
max_images: 5,
max_description_words: 500,
max_submissions: 10,
review_days: 3,
free_modification_days: 30,
},
premium: {
max_images: null, // unlimited
max_description_words: null, // unlimited
max_submissions: null, // unlimited
review_days: 1,
free_modification_days: 365,
},
};

Access Check Functions

canAccessFeature(feature: Feature, userPlan: string): boolean

Core access check that evaluates the feature access rule:

import { canAccessFeature, FEATURES } from '@/lib/guards';

canAccessFeature(FEATURES.UPLOAD_VIDEO, 'premium'); // true
canAccessFeature(FEATURES.UPLOAD_VIDEO, 'standard'); // false
canAccessFeature(FEATURES.UPLOAD_IMAGES, 'free'); // true ('all')
canAccessFeature(FEATURES.VERIFIED_BADGE, 'standard'); // true (minPlan: standard)
canAccessFeature(FEATURES.VERIFIED_BADGE, 'free'); // false

getFeatureLimit<K>(limitName: K, userPlan: string): FeatureLimits[K]

Returns the limit value for a plan:

import { getFeatureLimit } from '@/lib/guards';

getFeatureLimit('max_images', 'free'); // 1
getFeatureLimit('max_images', 'premium'); // null (unlimited)
getFeatureLimit('review_days', 'standard'); // 3

isWithinLimit(limitName, value, userPlan): boolean

Checks if a value is within the plan's limit:

import { isWithinLimit } from '@/lib/guards';

isWithinLimit('max_images', 3, 'free'); // false (limit: 1)
isWithinLimit('max_images', 3, 'standard'); // true (limit: 5)
isWithinLimit('max_images', 100, 'premium'); // true (unlimited)

getAccessibleFeatures(userPlan: string): Feature[]

Returns all features accessible by a given plan.

getMinimumPlanForFeature(feature: Feature): PaymentPlan

Returns the lowest plan that can access a feature.

Plan Guard Factory

createPlanGuard(userPlan: string)

Creates a guard instance with bound methods for a specific plan:

import { createPlanGuard, FEATURES } from '@/lib/guards';

const guard = createPlanGuard('standard');

// Check access
guard.canAccess(FEATURES.VERIFIED_BADGE); // true
guard.canAccess(FEATURES.PHONE_SUPPORT); // false

// Require access (throws PlanGuardError)
guard.requireFeature(FEATURES.VERIFIED_BADGE); // OK
guard.requireFeature(FEATURES.PHONE_SUPPORT); // throws!

// Limits
guard.getLimit('max_images'); // 5
guard.isWithinLimit('max_images', 3); // true
guard.requireWithinLimit('max_images', 3); // OK
guard.requireWithinLimit('max_images', 10); // throws!

// Info
guard.getAccessibleFeatures(); // Feature[]
guard.getPlan(); // 'standard'
guard.getPlanLevel(); // 2

PlanGuardError

Custom error thrown by requireFeature:

class PlanGuardError extends Error {
readonly feature: Feature;
readonly userPlan: string;
readonly requiredPlan: PaymentPlan;
}

Error Handling Example

import { createPlanGuard, PlanGuardError, FEATURES } from '@/lib/guards';

try {
const guard = createPlanGuard(userPlan);
guard.requireFeature(FEATURES.UPLOAD_VIDEO);
// Proceed with video upload
} catch (error) {
if (error instanceof PlanGuardError) {
return NextResponse.json({
error: `Upgrade to ${error.requiredPlan} to access this feature`,
requiredPlan: error.requiredPlan,
}, { status: 403 });
}
throw error;
}

React Hook Helper

PlanGuardResult Interface

interface PlanGuardResult {
canAccess: (feature: Feature) => boolean;
getLimit: <K extends keyof FeatureLimits>(limitName: K) => FeatureLimits[K];
isWithinLimit: (limitName: keyof FeatureLimits, value: number) => boolean;
accessibleFeatures: Feature[];
}

createPlanGuardResult(userPlan: string): PlanGuardResult

Creates a plain object suitable for use in React hooks and components:

import { createPlanGuardResult, FEATURES } from '@/lib/guards';

// In a custom hook
function usePlanGuard(plan: string) {
return useMemo(() => createPlanGuardResult(plan), [plan]);
}

// In a component
function SubmissionForm({ userPlan }) {
const guard = usePlanGuard(userPlan);

return (
<form>
<ImageUploader
maxImages={guard.getLimit('max_images') ?? Infinity}
disabled={!guard.canAccess(FEATURES.UPLOAD_IMAGES)}
/>
{guard.canAccess(FEATURES.UPLOAD_VIDEO) && <VideoUploader />}
</form>
);
}