Skip to content

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.

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
  • feature - Defines available features (e.g., cash_register, invoices)
  • subscription_plan - Subscription plans (e.g., Free, Pro)
  • plan_feature - Links features to subscription plans
  • organization_subscription - Organization’s active subscription
  • organization_feature_override - Manual feature overrides (optional)
  • feature_menu - Menu configuration for features
  • feature_menu_item - Individual menu items (URLs are used for route protection)
  • feature_route - Prefix routes for catch-all patterns (optional)
  1. User navigates to a route (e.g., /cash-register/invoices)
  2. Route’s beforeLoad hook calls checkRouteAccess()
  3. Backend checks feature_menu_item.url for exact match
  4. If not found, checks feature_route.route_prefix for prefix match
  5. Gets feature key and checks if organization has access
  6. Redirects to home if access denied

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;
}
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}`),
);

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.

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.

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>
);
}

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,
});

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.

Terminal window
# Push schema changes to database
pnpm db:push
# Seed the database with your new feature
pnpm db:seed

Complete 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 feature
const 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 plan
const linksToCreate = [
// ... existing
{ planId: proPlanId, featureId: inventoryFeatureId },
].filter(/* ... */);
// Menu configuration
const 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 },
];

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)

Routes are automatically protected based on feature_menu_item.url:

  1. Exact Match: /inventory/items is found in feature_menu_item.url → Gets feature key → Checks access
  2. Prefix Match: /inventory/custom-route is not in menu items → Checks feature_route.route_prefix → Finds /inventory → Gets feature key → Checks access
  1. Get feature key from route (via menu item or prefix route)
  2. Check if feature is enabled for organization:
    • Check organization_feature_override (manual override)
    • Check organization_subscriptionplan_feature (subscription plan)
  3. Check user’s role (for ‘manage’ action, requires owner/admin)
  4. Allow or deny access

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)

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
  1. Login as free user (no subscription)
  2. Navigate to your feature route directly (e.g., /my-feature/dashboard)
  3. Should be redirected to home page
  4. Feature should NOT appear in menu
  1. Login as admin user (with Pro subscription)
  2. Navigate to your feature route
  3. Should load successfully
  4. Feature SHOULD appear in menu
  1. Try accessing route directly via URL
  2. Check browser console for access denial messages
  3. Verify redirect to home page works

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 file
export const Route = createFileRoute('/my-feature')({
beforeLoad: async ({ location }) => {
await checkRouteAccess(location.pathname);
},
component: MyFeaturePage,
});
// 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 checkRouteAccess
// 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,
}
  • Check that checkRouteAccess() is in beforeLoad hook
  • Verify menu item URL matches route path exactly
  • Check that feature is linked to subscription plan in seed
  • Verify isFree: true OR organization has subscription with feature
  • Check menu configuration in seed file
  • Verify feature is enabled for organization
  • Check organization has active subscription
  • Verify feature is linked to subscription plan
  • Check organization_feature_override if exists
  • Verify user has active organization in session
  1. Use descriptive feature keys: Use snake_case (e.g., cash_register, inventory_management)
  2. Consistent route paths: Use kebab-case for routes (e.g., /cash-register/invoices)
  3. Always protect routes: Add checkRouteAccess() to all feature routes
  4. Menu items define routes: Let menu items define exact routes, use feature_route only for prefixes
  5. Test both free and subscribed users: Ensure access control works correctly
  6. Use shared components: Put reusable components in packages/features for web and native
  • 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
  • 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