Add subscription tier system to gate features behind Free/Pro/Enterprise tiers. Backend: - Create feature-tiers.ts with FEATURE_TIERS config and utilities - Add /api/config/feature-tiers endpoint for frontend config fetch - Create requireTier middleware for route-level tier enforcement - Add subscriptionTier to request.userContext in auth plugin - Gate scanForMaintenance in documents controller (Pro+ required) - Add migration to reset scanForMaintenance for free users Frontend: - Create useTierAccess hook for tier checking - Create UpgradeRequiredDialog component (responsive) - Gate DocumentForm checkbox with lock icon for free users - Add SubscriptionTier type to profile.types.ts Documentation: - Add TIER-GATING.md with usage guide Tests: 30 passing (feature-tiers, tier-guard, controller) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
9.5 KiB
Tier-Based Feature Gating
This document describes the user subscription tier system and how to gate features behind specific tiers.
Overview
MotoVaultPro supports three subscription tiers with hierarchical access:
| Tier | Level | Description |
|---|---|---|
free |
0 | Default tier, limited features |
pro |
1 | Mid-tier, most features |
enterprise |
2 | Full access to all features |
Higher tiers automatically have access to all lower-tier features (tier hierarchy).
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Frontend │
│ ┌─────────────────┐ ┌──────────────────────────────┐ │
│ │ useTierAccess │───▶│ UpgradeRequiredDialog │ │
│ │ hook │ │ component │ │
│ └────────┬────────┘ └──────────────────────────────┘ │
│ │ │
│ ▼ fetches │
├─────────────────────────────────────────────────────────────┤
│ Backend API │
│ ┌─────────────────┐ ┌──────────────────────────────┐ │
│ │ /api/config/ │ │ /api/documents │ │
│ │ feature-tiers │ │ (tier validation) │ │
│ └────────┬────────┘ └────────┬─────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ feature-tiers.ts (single source of truth) │ │
│ │ - FEATURE_TIERS config │ │
│ │ - canAccessFeature(), getTierLevel(), etc. │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Backend Implementation
Feature Configuration
All gated features are defined in backend/src/core/config/feature-tiers.ts:
export const FEATURE_TIERS = {
'document.scanMaintenanceSchedule': {
minTier: 'pro',
name: 'Scan for Maintenance Schedule',
upgradePrompt: 'Upgrade to Pro to automatically extract maintenance schedules from your manuals.'
},
// Add new features here
} as const;
Utility Functions
import {
canAccessFeature,
getTierLevel,
getRequiredTier,
getFeatureConfig
} from '../core/config/feature-tiers';
// Check if user can access a feature
canAccessFeature('pro', 'document.scanMaintenanceSchedule'); // true
canAccessFeature('free', 'document.scanMaintenanceSchedule'); // false
// Get numeric tier level for comparison
getTierLevel('pro'); // 1
// Get minimum required tier for a feature
getRequiredTier('document.scanMaintenanceSchedule'); // 'pro'
Route-Level Protection (Middleware)
Use requireTier for routes that require a minimum tier:
// In routes file
fastify.post('/premium-endpoint', {
preHandler: [fastify.requireTier({ minTier: 'pro' })],
handler: controller.premiumAction
});
// Or by feature key
fastify.post('/scan-maintenance', {
preHandler: [fastify.requireTier({ featureKey: 'document.scanMaintenanceSchedule' })],
handler: controller.scanMaintenance
});
Controller-Level Validation
For conditional feature checks within a controller:
async create(request: FastifyRequest, reply: FastifyReply) {
const userTier = request.userContext?.subscriptionTier || 'free';
if (request.body.scanForMaintenance && !canAccessFeature(userTier, 'document.scanMaintenanceSchedule')) {
const config = getFeatureConfig('document.scanMaintenanceSchedule');
return reply.code(403).send({
error: 'TIER_REQUIRED',
requiredTier: config?.minTier || 'pro',
currentTier: userTier,
feature: 'document.scanMaintenanceSchedule',
featureName: config?.name || null,
upgradePrompt: config?.upgradePrompt || 'Upgrade to access this feature.',
});
}
// ... continue with operation
}
403 Error Response Format
{
"error": "TIER_REQUIRED",
"requiredTier": "pro",
"currentTier": "free",
"feature": "document.scanMaintenanceSchedule",
"featureName": "Scan for Maintenance Schedule",
"upgradePrompt": "Upgrade to Pro to automatically extract maintenance schedules from your manuals."
}
Frontend Implementation
useTierAccess Hook
import { useTierAccess } from '@/core/hooks';
const MyComponent = () => {
const { tier, loading, hasAccess, checkAccess } = useTierAccess();
// Simple boolean check
if (!hasAccess('document.scanMaintenanceSchedule')) {
// Show upgrade prompt or disable feature
}
// Detailed access info
const access = checkAccess('document.scanMaintenanceSchedule');
// {
// allowed: false,
// requiredTier: 'pro',
// config: { name: '...', upgradePrompt: '...' }
// }
};
UpgradeRequiredDialog Component
import { UpgradeRequiredDialog } from '@/shared-minimal/components';
const MyComponent = () => {
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
const { hasAccess } = useTierAccess();
return (
<>
<button
disabled={!hasAccess('document.scanMaintenanceSchedule')}
onClick={() => {
if (!hasAccess('document.scanMaintenanceSchedule')) {
setUpgradeDialogOpen(true);
} else {
// Perform action
}
}}
>
Premium Feature
</button>
<UpgradeRequiredDialog
featureKey="document.scanMaintenanceSchedule"
open={upgradeDialogOpen}
onClose={() => setUpgradeDialogOpen(false)}
/>
</>
);
};
Gating a Checkbox (Example)
const { hasAccess } = useTierAccess();
const canScanMaintenance = hasAccess('document.scanMaintenanceSchedule');
<label className={canScanMaintenance ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'}>
<input
type="checkbox"
checked={canScanMaintenance ? scanForMaintenance : false}
onChange={(e) => canScanMaintenance && setScanForMaintenance(e.target.checked)}
disabled={!canScanMaintenance}
/>
<span>Scan for Maintenance Schedule</span>
</label>
{!canScanMaintenance && (
<button onClick={() => setUpgradeDialogOpen(true)}>
<LockIcon />
</button>
)}
Adding a New Gated Feature
Step 1: Add to Feature Config (Backend)
// backend/src/core/config/feature-tiers.ts
export const FEATURE_TIERS = {
// ... existing features
'reports.advancedAnalytics': {
minTier: 'enterprise',
name: 'Advanced Analytics',
upgradePrompt: 'Upgrade to Enterprise for advanced fleet analytics and reporting.'
},
} as const;
Step 2: Add Backend Validation
Either use middleware on the route:
fastify.get('/reports/analytics', {
preHandler: [fastify.requireTier({ featureKey: 'reports.advancedAnalytics' })],
handler: controller.getAnalytics
});
Or validate in the controller for conditional features.
Step 3: Add Frontend Check
const { hasAccess } = useTierAccess();
if (!hasAccess('reports.advancedAnalytics')) {
return <UpgradePrompt featureKey="reports.advancedAnalytics" />;
}
API Endpoint
The frontend fetches tier configuration from:
GET /api/config/feature-tiers
Response:
{
"tiers": { "free": 0, "pro": 1, "enterprise": 2 },
"features": {
"document.scanMaintenanceSchedule": {
"minTier": "pro",
"name": "Scan for Maintenance Schedule",
"upgradePrompt": "..."
}
}
}
User Tier Management
Admin UI
Admins can change user tiers via the Admin Users page dropdown.
Database
User tiers are stored in user_profiles.subscription_tier column (enum: free, pro, enterprise).
Auth Context
The user's tier is included in request.userContext.subscriptionTier after authentication.
Testing
Backend Tests
# Run tier-related tests
npm test -- --testPathPattern="feature-tiers|tier-guard|documents.controller.tier"
Test Cases to Cover
- Free user attempting to use Pro feature returns 403
- Pro user can use Pro features
- Enterprise user can use all features
- Unknown features are allowed (fail open)
- Missing userContext defaults to free tier
Future Considerations
- Stripe billing integration for tier upgrades
- Subscription expiration handling
- Grace periods for downgraded users
- Feature usage analytics