# 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`:
```typescript
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
```typescript
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:
```typescript
// 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:
```typescript
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
```json
{
"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
```typescript
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
```tsx
import { UpgradeRequiredDialog } from '@/shared-minimal/components';
const MyComponent = () => {
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
const { hasAccess } = useTierAccess();
return (
<>
setUpgradeDialogOpen(false)}
/>
>
);
};
```
### Gating a Checkbox (Example)
```tsx
const { hasAccess } = useTierAccess();
const canScanMaintenance = hasAccess('document.scanMaintenanceSchedule');
{!canScanMaintenance && (
)}
```
## Adding a New Gated Feature
### Step 1: Add to Feature Config (Backend)
```typescript
// 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:
```typescript
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
```tsx
const { hasAccess } = useTierAccess();
if (!hasAccess('reports.advancedAnalytics')) {
return ;
}
```
## 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
```bash
# Run tier-related tests
npm test -- --testPathPattern="feature-tiers|tier-guard|documents.controller.tier"
```
### Test Cases to Cover
1. Free user attempting to use Pro feature returns 403
2. Pro user can use Pro features
3. Enterprise user can use all features
4. Unknown features are allowed (fail open)
5. 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