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
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>
312 lines
9.5 KiB
Markdown
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
|