Implementing New Features
Implementing New Features
Section titled “Implementing New Features”This guide walks you through implementing a new feature in the AnyBiz application, including database setup, route protection, menu configuration, and subscription integration.
Overview
Section titled “Overview”The feature system in AnyBiz provides:
- Subscription-based access control - Features can be enabled/disabled based on subscription plans
- Route-level protection - Routes are automatically protected based on feature requirements
- Dynamic menu generation - Menu items are automatically shown/hidden based on enabled features
- Database-driven configuration - All feature configuration is stored in the database
Architecture
Section titled “Architecture”Database Tables
Section titled “Database Tables”feature- Defines available features (e.g.,cash_register,invoices)subscription_plan- Subscription plans (e.g., Free, Pro)plan_feature- Links features to subscription plansorganization_subscription- Organization’s active subscriptionorganization_feature_override- Manual feature overrides (optional)feature_menu- Menu configuration for featuresfeature_menu_item- Individual menu items (URLs are used for route protection)feature_route- Prefix routes for catch-all patterns (optional)
Route Protection Flow
Section titled “Route Protection Flow”- User navigates to a route (e.g.,
/cash-register/invoices) - Route’s
beforeLoadhook callscheckRouteAccess() - Backend checks
feature_menu_item.urlfor exact match - If not found, checks
feature_route.route_prefixfor prefix match - Gets feature key and checks if organization has access
- Redirects to home if access denied
Step-by-Step Guide
Section titled “Step-by-Step Guide”Step 1: Create the Feature in Database
Section titled “Step 1: Create the Feature in Database”Add your feature to the seed file (packages/db/src/seed.ts):
// In the features section, add:const myFeature = existingFeatures.find((f) => f.key === 'my_feature');
if (!myFeature) { myFeatureId = randomUUID(); featuresToCreate.push({ id: myFeatureId, key: 'my_feature', name: 'My Feature', description: 'Description of my feature', });} else { myFeatureId = myFeature.id;}Step 2: Link Feature to Subscription Plans
Section titled “Step 2: Link Feature to Subscription Plans”In the same seed file, add the feature to subscription plans:
// In the planFeature links section:const linksToCreate = [ // ... existing links { planId: proPlanId, featureId: myFeatureId }, // Add to Pro plan // Note: Free features should be added to freePlanId].filter( (link) => !existingLinks.has(`${link.planId}-${link.featureId}`),);Step 3: Create Menu Configuration
Section titled “Step 3: Create Menu Configuration”Add menu configuration in the seed file:
// In the menuConfigs array:{ featureId: myFeatureId, featureKey: 'my_feature', title: 'My Feature', icon: 'IconName', // Lucide icon name isFree: false, // Set to true for free features items: [ { title: 'Dashboard', url: '/my-feature/dashboard' }, { title: 'Settings', url: '/my-feature/settings' }, ],}Important: The url values in menu items are automatically used for route protection. No need to add them to feature_route unless you need prefix matching.
Step 4: Add Prefix Routes (Optional)
Section titled “Step 4: Add Prefix Routes (Optional)”If you need catch-all route protection (e.g., /my-feature/*), add to feature_route:
// In the routeMappings array:{ routePrefix: '/my-feature', // Matches /my-feature/* featureId: myFeatureId, order: 0,}Note: Exact routes (like /my-feature/dashboard) are automatically protected via feature_menu_item.url, so you only need prefix routes for catch-all patterns.
Step 5: Create Frontend Routes
Section titled “Step 5: Create Frontend Routes”5.1 Create Shared Component (Optional)
Section titled “5.1 Create Shared Component (Optional)”If the component is shared between web and native apps, create it in packages/features/src/routes/my-feature/dashboard.tsx:
/** * My Feature Dashboard page component * This component is shared between web and os_native apps */export function MyFeatureDashboardPage() { return ( <div className="container mx-auto py-8"> <h1 className="text-3xl font-bold mb-4">My Feature - Dashboard</h1> <p className="text-muted-foreground"> This is the My Feature dashboard page. </p> </div> );}5.2 Create Web Route
Section titled “5.2 Create Web Route”Create apps/web/src/routes/my-feature/dashboard.tsx:
import { createFileRoute } from '@tanstack/react-router';import { MyFeatureDashboardPage } from '@repo/features/routes/my-feature/dashboard';import { checkRouteAccess } from '@/utils/route-protection';
export const Route = createFileRoute('/my-feature/dashboard')({ beforeLoad: async ({ location }) => { await checkRouteAccess(location.pathname); }, component: MyFeatureDashboardPage,});5.3 Create Native Route
Section titled “5.3 Create Native Route”Create apps/os_native/src/routes/my-feature/dashboard.tsx:
import { createFileRoute } from '@tanstack/react-router';import { MyFeatureDashboardPage } from '@repo/features/routes/my-feature/dashboard';import { checkRouteAccess } from '@/utils/route-protection';
export const Route = createFileRoute('/my-feature/dashboard')({ beforeLoad: async ({ location }) => { await checkRouteAccess(location.pathname); }, component: MyFeatureDashboardPage,});Important: Always add checkRouteAccess() in the beforeLoad hook to protect the route.
Step 6: Run Database Migration and Seed
Section titled “Step 6: Run Database Migration and Seed”# Push schema changes to databasepnpm db:push
# Seed the database with your new featurepnpm db:seedComplete Example: Adding “Inventory” Feature
Section titled “Complete Example: Adding “Inventory” Feature”Here’s a complete example of adding an “Inventory” feature:
1. Database Seed (packages/db/src/seed.ts)
Section titled “1. Database Seed (packages/db/src/seed.ts)”// Find or create featureconst inventoryFeature = existingFeatures.find((f) => f.key === 'inventory');
if (!inventoryFeature) { inventoryFeatureId = randomUUID(); featuresToCreate.push({ id: inventoryFeatureId, key: 'inventory', name: 'Inventory Management', description: 'Track and manage inventory items', });} else { inventoryFeatureId = inventoryFeature.id;}
// Link to Pro planconst linksToCreate = [ // ... existing { planId: proPlanId, featureId: inventoryFeatureId },].filter(/* ... */);
// Menu configurationconst menuConfigs = [ // ... existing { featureId: inventoryFeatureId, featureKey: 'inventory', title: 'Inventory', icon: 'Package', isFree: false, items: [ { title: 'Items', url: '/inventory/items' }, { title: 'Stock', url: '/inventory/stock' }, { title: 'Reports', url: '/inventory/reports' }, ], },];
// Prefix route (optional - for catch-all)const routeMappings = [ // ... existing { routePrefix: '/inventory', featureId: inventoryFeatureId, order: 0 },];2. Frontend Routes
Section titled “2. Frontend Routes”Web: apps/web/src/routes/inventory/items.tsx
import { createFileRoute } from '@tanstack/react-router';import { InventoryItemsPage } from '@repo/features/routes/inventory/items';import { checkRouteAccess } from '@/utils/route-protection';
export const Route = createFileRoute('/inventory/items')({ beforeLoad: async ({ location }) => { await checkRouteAccess(location.pathname); }, component: InventoryItemsPage,});Native: apps/os_native/src/routes/inventory/items.tsx (same structure)
How Route Protection Works
Section titled “How Route Protection Works”Automatic Route Detection
Section titled “Automatic Route Detection”Routes are automatically protected based on feature_menu_item.url:
- Exact Match:
/inventory/itemsis found infeature_menu_item.url→ Gets feature key → Checks access - Prefix Match:
/inventory/custom-routeis not in menu items → Checksfeature_route.route_prefix→ Finds/inventory→ Gets feature key → Checks access
Access Check Process
Section titled “Access Check Process”- Get feature key from route (via menu item or prefix route)
- Check if feature is enabled for organization:
- Check
organization_feature_override(manual override) - Check
organization_subscription→plan_feature(subscription plan)
- Check
- Check user’s role (for ‘manage’ action, requires owner/admin)
- Allow or deny access
Free vs Subscription Features
Section titled “Free vs Subscription Features”Free Features
Section titled “Free Features”Set isFree: true in menu configuration:
{ featureId: invoicesFeatureId, featureKey: 'invoices', title: 'Invoices', icon: 'FileText', isFree: true, // Free feature items: [ { title: 'List', url: '/invoice/invoices' }, ],}Free features:
- Available to all users (no subscription required)
- Still appear in menu
- Routes are still protected (but access is always granted)
Subscription Features
Section titled “Subscription Features”Set isFree: false in menu configuration:
{ featureId: cashRegisterFeatureId, featureKey: 'cash_register', title: 'Cash Register', icon: 'Calculator', isFree: false, // Subscription required items: [ { title: 'Invoices', url: '/cash-register/invoices' }, ],}Subscription features:
- Only available to organizations with active subscription
- Menu items only shown if feature is enabled
- Routes are protected and require subscription
Testing Your Feature
Section titled “Testing Your Feature”1. Test with Free User
Section titled “1. Test with Free User”- Login as free user (no subscription)
- Navigate to your feature route directly (e.g.,
/my-feature/dashboard) - Should be redirected to home page
- Feature should NOT appear in menu
2. Test with Subscribed User
Section titled “2. Test with Subscribed User”- Login as admin user (with Pro subscription)
- Navigate to your feature route
- Should load successfully
- Feature SHOULD appear in menu
3. Test Route Protection
Section titled “3. Test Route Protection”- Try accessing route directly via URL
- Check browser console for access denial messages
- Verify redirect to home page works
Common Patterns
Section titled “Common Patterns”Pattern 1: Simple Feature with Single Route
Section titled “Pattern 1: Simple Feature with Single Route”// Menu config{ featureId: myFeatureId, featureKey: 'my_feature', title: 'My Feature', icon: 'Icon', isFree: false, items: [ { title: 'Dashboard', url: '/my-feature' }, ],}
// Route fileexport const Route = createFileRoute('/my-feature')({ beforeLoad: async ({ location }) => { await checkRouteAccess(location.pathname); }, component: MyFeaturePage,});Pattern 2: Feature with Multiple Routes
Section titled “Pattern 2: Feature with Multiple Routes”// Menu config{ featureId: myFeatureId, featureKey: 'my_feature', title: 'My Feature', icon: 'Icon', isFree: false, items: [ { title: 'Dashboard', url: '/my-feature/dashboard' }, { title: 'Settings', url: '/my-feature/settings' }, { title: 'Reports', url: '/my-feature/reports' }, ],}
// Each route needs its own file with checkRouteAccessPattern 3: Feature with Catch-All Routes
Section titled “Pattern 3: Feature with Catch-All Routes”// Menu config (for main routes){ featureId: myFeatureId, featureKey: 'my_feature', title: 'My Feature', icon: 'Icon', isFree: false, items: [ { title: 'Dashboard', url: '/my-feature/dashboard' }, ],}
// Prefix route for catch-all{ routePrefix: '/my-feature', // Protects /my-feature/* featureId: myFeatureId, order: 0,}Troubleshooting
Section titled “Troubleshooting”Route Not Protected
Section titled “Route Not Protected”- Check that
checkRouteAccess()is inbeforeLoadhook - Verify menu item URL matches route path exactly
- Check that feature is linked to subscription plan in seed
Feature Not in Menu
Section titled “Feature Not in Menu”- Verify
isFree: trueOR organization has subscription with feature - Check menu configuration in seed file
- Verify feature is enabled for organization
Access Denied for Subscribed User
Section titled “Access Denied for Subscribed User”- Check organization has active subscription
- Verify feature is linked to subscription plan
- Check
organization_feature_overrideif exists - Verify user has active organization in session
Best Practices
Section titled “Best Practices”- Use descriptive feature keys: Use snake_case (e.g.,
cash_register,inventory_management) - Consistent route paths: Use kebab-case for routes (e.g.,
/cash-register/invoices) - Always protect routes: Add
checkRouteAccess()to all feature routes - Menu items define routes: Let menu items define exact routes, use
feature_routeonly for prefixes - Test both free and subscribed users: Ensure access control works correctly
- Use shared components: Put reusable components in
packages/featuresfor web and native
Summary Checklist
Section titled “Summary Checklist”- Feature created in database seed
- Feature linked to subscription plan(s)
- Menu configuration added
- Prefix route added (if needed)
- Frontend routes created (web + native)
- Route protection added (
checkRouteAccess) - Database migration run (
pnpm db:push) - Database seeded (
pnpm db:seed) - Tested with free user (should be blocked)
- Tested with subscribed user (should work)
- Feature appears in menu for subscribed users
Related Files
Section titled “Related Files”- Database schema:
packages/db/src/schemas/subscription.ts - Seed file:
packages/db/src/seed.ts - Route protection:
apps/web/src/utils/route-protection.ts/apps/os_native/src/utils/route-protection.ts - Feature access check:
packages/api/src/server/utils/feature-access.ts - Route-to-feature mapping:
packages/api/src/server/utils/route-features.ts