Files
motovaultpro/docs/TIER-GATING.md
Eric Gullickson f494f77150
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m35s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 27s
Deploy to Staging / Verify Staging (pull_request) Successful in 5s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 5s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
feat: Implement user tier-based feature gating system (refs #8)
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>
2026-01-04 14:34:47 -06:00

312 lines
9.5 KiB
Markdown

# 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 (
<>
<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)
```tsx
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)
```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 <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
```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