Navigation & Menu Configuration Documentation
Module: Navigation Structure & Menu Management
Version: 1.0
Last Updated: March 12, 2026
📌 Overview
Navigation Configuration manages the application menu structure, navigation paths, and menu item hierarchy. It defines:
- Menu items: labels, icons, routes, permissions
- Menu hierarchy: parent-child relationships for submenus
- Navigation rules: visibility conditions, role-based access
- Breadcrumbs & navigation paths: user context and navigation history
Core Table:
app_navigation_configs- Navigation menu definitions
Related:
app_roles- Role-based menu visibility (includingrol_navigation_access; see below)app_page_configs- Target pages for menu items
Role-wise navigation (app_roles.rol_navigation_access)
Per-role navigation entry access is stored as JSON on app_roles.rol_navigation_access (nullable). It lists top-level navigation codes (nav_code / row code), not nested menuItemId values inside nav_config.
- Schema & examples: TENANT_USERS_ROLES.md (
rol_navigation_accesssection). - DDL: repo migration
003_app_roles_rol_navigation_access.sql. - Runtime filtering: implement inside
fn_get_app_navigation_configs_by_tenant_role(or equivalent) by joiningapp_rolesfor the current@rol_code/ tenant and applying allowlist/denylist. Reusable pattern:003_app_roles_rol_navigation_access_TVF_FILTER_PATTERN.sql.
Precedence: nav_config / visibleTo vs rol_navigation_access
Menu JSON may include visibleTo on items (examples below). When rol_navigation_access is non-NULL, treat effective visibility as the intersection of:
- Whatever the navigation definition / TVF already enforces (including
visibleToor equivalent), and - The allowlist or denylist from
rol_navigation_access.
If rol_navigation_access is NULL, only (1) applies. This keeps navigation config as the broad ceiling and roles as an additional tightening layer.
app_navigation_configs - Menu Definitions
Purpose
Stores complete navigation menu structures including hierarchy, icons, routes, and access control rules.
Schema
SELECT
nav_id (INT), -- Primary Key
nav_tenant_id (INT), -- Tenant isolation
nav_code (VARCHAR(256)), -- Menu identifier (e.g., 'MAIN_MENU')
nav_description (VARCHAR), -- Display name
nav_config (JSON), -- Complete menu definition
nav_order (INT), -- Display order
nav_status (INT), -- 1=Active, 0=Inactive
nav_is_deleted (BIT), -- Soft delete flag
nav_audit_data (JSON), -- Audit trail
nav_add_dtls (JSON) -- Additional metadata
FROM app_navigation_configs
WHERE nav_status = 1 AND nav_is_deleted = 0
ORDER BY nav_order;
nav_config Structure (JSON) - Complete Navigation Definition
{
"navigationId": "MAIN_MENU",
"navigationName": "Main Application Menu",
"navigationDescription": "Primary navigation for all users",
"display": {
"menuLayout": "sidebar", // 'sidebar', 'topbar', 'drawer'
"expandable": true,
"collapsible": true,
"showIcons": true,
"showLabels": true,
"maxDepth": 3, // Maximum nesting level
"defaultExpanded": true
},
"menuItems": [
{
"menuItemId": "menu_dashboard",
"menuItemName": "Dashboard",
"menuItemOrder": 1,
"icon": "dashboard",
"route": "/dashboard",
"visibleTo": ["ALL"], // Roles: ALL, ADMIN, MANAGER, USER
"isActive": true,
"badge": null,
"description": "View dashboard and overview",
"children": null
},
{
"menuItemId": "menu_customers",
"menuItemName": "Customers",
"menuItemOrder": 2,
"icon": "people",
"route": null, // Has children
"visibleTo": ["ADMIN", "MANAGER"],
"isActive": true,
"badge": null,
"description": "Customer management",
"children": [
{
"menuItemId": "menu_customers_list",
"menuItemName": "Customer List",
"menuItemOrder": 1,
"icon": "list",
"route": "/customers/list",
"visibleTo": ["ADMIN", "MANAGER"],
"isActive": true,
"children": null
},
{
"menuItemId": "menu_customers_new",
"menuItemName": "Add New",
"menuItemOrder": 2,
"icon": "plus",
"route": "/customers/new",
"visibleTo": ["ADMIN"],
"isActive": true,
"children": null
},
{
"menuItemId": "menu_customers_groups",
"menuItemName": "Customer Groups",
"menuItemOrder": 3,
"icon": "folder",
"route": "/customers/groups",
"visibleTo": ["ADMIN"],
"isActive": true,
"children": null
}
]
},
{
"menuItemId": "menu_orders",
"menuItemName": "Orders",
"menuItemOrder": 3,
"icon": "shopping_cart",
"route": "/orders/list",
"visibleTo": ["ADMIN", "MANAGER", "SALES"],
"isActive": true,
"badge": {
"value": 3, // Number badge
"color": "error",
"type": "dot" // 'dot', 'count', 'avatar'
},
"children": null
},
{
"menuItemId": "menu_configure",
"menuItemName": "Configuration",
"menuItemOrder": 4,
"icon": "settings",
"route": null,
"visibleTo": ["ADMIN"],
"isActive": true,
"children": [
{
"menuItemId": "menu_users",
"menuItemName": "Users & Roles",
"menuItemOrder": 1,
"icon": "admin_panel_settings",
"route": "/config/users",
"visibleTo": ["ADMIN"],
"isActive": true
},
{
"menuItemId": "menu_entities",
"menuItemName": "Entity Registry",
"menuItemOrder": 2,
"icon": "database",
"route": "/config/entities",
"visibleTo": ["ADMIN"],
"isActive": true
},
{
"menuItemId": "menu_designer",
"menuItemName": "Designers",
"menuItemOrder": 3,
"icon": "design_services",
"route": null,
"visibleTo": ["ADMIN"],
"children": [
{
"menuItemId": "menu_form_designer",
"menuItemName": "Form Designer",
"menuItemOrder": 1,
"icon": "edit_note",
"route": "/designer/forms",
"visibleTo": ["ADMIN"]
},
{
"menuItemId": "menu_dataview_designer",
"menuItemName": "DataView Designer",
"menuItemOrder": 2,
"icon": "table_chart",
"route": "/designer/dataviews",
"visibleTo": ["ADMIN"]
},
{
"menuItemId": "menu_workflow_designer",
"menuItemName": "Workflow Designer",
"menuItemOrder": 3,
"icon": "account_tree",
"route": "/designer/workflows",
"visibleTo": ["ADMIN"]
}
]
},
{
"menuItemId": "menu_system",
"menuItemName": "System",
"menuItemOrder": 4,
"icon": "admin_panel_settings",
"route": "/config/system",
"visibleTo": ["ADMIN"],
"isActive": true
}
]
},
{
"menuItemId": "menu_reports",
"menuItemName": "Reports",
"menuItemOrder": 5,
"icon": "assessment",
"route": null,
"visibleTo": ["ADMIN", "MANAGER", "ANALYST"],
"isActive": true,
"children": [
{
"menuItemId": "menu_sales_reports",
"menuItemName": "Sales Reports",
"menuItemOrder": 1,
"icon": "trending_up",
"route": "/reports/sales",
"visibleTo": ["ADMIN", "MANAGER", "ANALYST"]
},
{
"menuItemId": "menu_customer_reports",
"menuItemName": "Customer Reports",
"menuItemOrder": 2,
"icon": "group",
"route": "/reports/customers",
"visibleTo": ["ADMIN", "MANAGER"]
}
]
},
{
"menuItemId": "menu_help",
"menuItemName": "Help & Support",
"menuItemOrder": 6,
"icon": "help",
"route": "https://help.example.com",
"externalLink": true,
"visibleTo": ["ALL"],
"target": "_blank",
"isActive": true
}
],
"contextMenu": {
"enableContextMenu": true,
"menuItems": [
{
"contextMenuItemId": "ctx_edit",
"contextMenuItemName": "Edit",
"icon": "edit",
"action": "openEditForm"
},
{
"contextMenuItemId": "ctx_delete",
"contextMenuItemName": "Delete",
"icon": "delete",
"action": "deleteRecord"
}
]
},
"breadcrumbs": {
"enableBreadcrumbs": true,
"showHome": true,
"separator": "/"
}
}
Frontend Navigation Component Integration
Navigation Service
// From: axiom-genesis-frontend/src/services/navigation.service.ts
export class NavigationService {
static async getNavigationConfig(navCode: string): Promise<NavigationConfig> {
const response = await api.get(`/api/v1/navigation/${navCode}/config`);
return response.data.data;
}
static async getMenuItems(
navCode: string,
userRoles: string[],
): Promise<MenuItem[]> {
const config = await this.getNavigationConfig(navCode);
// Filter menu items based on user roles
return this.filterMenuByRole(config.menuItems, userRoles);
}
private static filterMenuByRole(
items: MenuItem[],
roles: string[],
): MenuItem[] {
return items
.filter((item) => {
// Show if "ALL" or user has one of the required roles
return (
item.visibleTo.includes("ALL") ||
item.visibleTo.some((role) => roles.includes(role))
);
})
.map((item) => ({
...item,
children: item.children
? this.filterMenuByRole(item.children, roles)
: null,
}))
.filter((item) => item.children === null || item.children.length > 0);
}
}
Layout / Sidebar Component
// From: axiom-genesis-frontend/src/components/Layout/Sidebar.tsx
export const Sidebar: React.FC = () => {
const dispatch = useAppDispatch();
const userRoles = useAppSelector(state => state.auth.user?.roles);
const [menuItems, setMenuItems] = useState<MenuItem[]>([]);
const [collapsed, setCollapsed] = useState(false);
useEffect(() => {
const loadMenu = async () => {
try {
const items = await NavigationService.getMenuItems('MAIN_MENU', userRoles);
setMenuItems(items);
} catch (error) {
console.error('Failed to load navigation config', error);
}
};
loadMenu();
}, [userRoles]);
return (
<aside className={`sidebar ${collapsed ? 'collapsed' : ''}`}>
<div className="sidebar-header">
<button onClick={() => setCollapsed(!collapsed)}>
{collapsed ? '→' : '←'}
</button>
</div>
<nav className="sidebar-menu">
{menuItems.map(item => (
<MenuItem
key={item.menuItemId}
item={item}
collapsed={collapsed}
depth={0}
/>
))}
</nav>
</aside>
);
};
// Recursive menu item component
const MenuItem: React.FC<{
item: MenuItem;
collapsed: boolean;
depth: number;
}> = ({ item, collapsed, depth }) => {
const router = useRouter();
const [expanded, setExpanded] = useState(false);
const hasChildren = item.children && item.children.length > 0;
const isActive = router.pathname === item.route ||
router.pathname.startsWith(item.route + '/');
const handleClick = () => {
if (hasChildren) {
setExpanded(!expanded);
} else if (item.externalLink) {
window.open(item.route, item.target || '_self');
} else {
router.push(item.route);
}
};
return (
<div className={`menu-item depth-${depth}`}>
<button
className={`menu-label ${isActive ? 'active' : ''}`}
onClick={handleClick}
title={item.menuItemName}
>
{item.icon && !collapsed && (
<i className={`icon icon-${item.icon}`} />
)}
{!collapsed && <span>{item.menuItemName}</span>}
{item.badge && <span className="badge">{item.badge.value}</span>}
{hasChildren && (
<i className={`expand-icon ${expanded ? 'up' : 'down'}`} />
)}
</button>
{hasChildren && expanded && !collapsed && (
<div className="menu-children">
{item.children.map(child => (
<MenuItem
key={child.menuItemId}
item={child}
collapsed={collapsed}
depth={depth + 1}
/>
))}
</div>
)}
</div>
);
};
Backend Navigation Service
Load Navigation Config
// From: axiom-genesis-middleware/services/navigation.service.js
NavigationService.getConfig = async (navCode, tenantId) => {
const db = getConnection();
// Try cache first
const cacheKey = `nav:${tenantId}:${navCode}`;
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Load from database
const result = await db
.request()
.input("nav_code", navCode)
.input("nav_tenant_id", tenantId).query(`
SELECT nav_config
FROM app_navigation_configs
WHERE nav_code = @nav_code
AND nav_tenant_id = @nav_tenant_id
AND nav_status = 1
AND nav_is_deleted = 0
`);
if (!result.recordset[0]) {
throw new NotFoundError("Navigation config not found");
}
const config = JSON.parse(result.recordset[0].nav_config);
// Cache for 24 hours
await redis.setex(cacheKey, 86400, JSON.stringify(config));
return config;
};
NavigationService.filterByRole = (config, userRoles) => {
const filterItems = (items) => {
return items
.filter((item) => {
// Show if "ALL" or user has one of required roles
return (
item.visibleTo.includes("ALL") ||
item.visibleTo.some((role) => userRoles.includes(role))
);
})
.map((item) => ({
...item,
children: item.children ? filterItems(item.children) : null,
}))
.filter((item) => item.children === null || item.children.length > 0);
};
return {
...config,
menuItems: filterItems(config.menuItems),
};
};
API Endpoint
// Route: GET /api/v1/navigation/:navCode/config
router.get("/navigation/:navCode/config", async (req, res) => {
try {
const { navCode } = req.params;
const tenantId = req.tenantId;
const userRoles = req.user.roles;
// Get config from cache/database
const config = await NavigationService.getConfig(navCode, tenantId);
// Filter by user roles
const filteredConfig = NavigationService.filterByRole(config, userRoles);
res.json({
success: true,
data: filteredConfig,
});
} catch (error) {
handleApiError(res, error);
}
});
Breadcrumb Navigation
Breadcrumb Service
// From: axiom-genesis-frontend/src/services/breadcrumb.service.ts
export interface Breadcrumb {
label: string;
route?: string;
active?: boolean;
}
export class BreadcrumbService {
static generateBreadcrumbs(pathname: string): Breadcrumb[] {
const segments = pathname.split("/").filter(Boolean);
const breadcrumbs: Breadcrumb[] = [{ label: "Home", route: "/" }];
let currentPath = "";
for (const segment of segments) {
currentPath += `/${segment}`;
breadcrumbs.push({
label: this.formatLabel(segment),
route: currentPath,
active: currentPath === pathname,
});
}
return breadcrumbs;
}
private static formatLabel(segment: string): string {
// Convert kebab-case to Title Case
return segment
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
static addBreadcrumb(label: string, route?: string): void {
// Dynamically add breadcrumbs (context-aware)
// Stored in Redux for global state
}
}
Breadcrumb Component
// From: axiom-genesis-frontend/src/components/Breadcrumbs.tsx
export const Breadcrumbs: React.FC = () => {
const router = useRouter();
const breadcrumbs = BreadcrumbService.generateBreadcrumbs(router.pathname);
return (
<nav className="breadcrumbs">
{breadcrumbs.map((crumb, index) => (
<React.Fragment key={index}>
{index > 0 && <span className="separator">/</span>}
{crumb.active ? (
<span className="breadcrumb-label active">{crumb.label}</span>
) : (
<Link href={crumb.route} className="breadcrumb-link">
{crumb.label}
</Link>
)}
</React.Fragment>
))}
</nav>
);
};
Queries for Navigation Configs
Get All Navigation Configs
SELECT
nav_code,
nav_description,
nav_order,
nav_status
FROM app_navigation_configs
WHERE nav_tenant_id = 1
AND nav_is_deleted = 0
ORDER BY nav_order;
Get Menu Structure
SELECT
nav_code,
JSON_EXTRACT(nav_config, '$.menuItems') as menu_structure,
JSON_EXTRACT(nav_config, '$.display') as display_settings
FROM app_navigation_configs
WHERE nav_code = 'MAIN_MENU'
AND nav_tenant_id = 1;
API Examples
Get Navigation Config
GET /api/v1/navigation/MAIN_MENU/config
Headers: {
'x-tenant-id': 'encrypted_value',
'Authorization': 'Bearer token'
}
Response:
{
"success": true,
"data": {
"navigationId": "MAIN_MENU",
"navigationName": "Main Application Menu",
"menuItems": [
{
"menuItemId": "menu_dashboard",
"menuItemName": "Dashboard",
"icon": "dashboard",
"route": "/dashboard",
"visibleTo": ["ALL"],
"children": null
},
...
]
}
}
Get Filtered Menu Items (by User Role)
GET /api/v1/navigation/MAIN_MENU/items?roles=MANAGER,ANALYST
Headers: {
'x-tenant-id': 'encrypted_value',
'Authorization': 'Bearer token'
}
Response:
{
"success": true,
"data": [
// Only menu items visible to MANAGER or ANALYST
]
}
Do's and Don'ts
✅ DO
- Use meaningful menu item labels - Clear, concise names
- Organize hierarchically - Group related items in submenus
- Assign icons to items - Visual aid for navigation
- Use role-based visibility - Control access via visibleTo array
- Limit menu depth - Usually 2-3 levels maximum
- Sort well-used items first - Common tasks at top
- Add breadcrumbs - Help users understand location
- Cache navigation configs - Frequently accessed, rarely changed
- Use external links appropriately - Only for truly external resources
❌ DON'T
- Create overly deep menus - More than 3 levels confuses users
- Hide all items from users - Ensure each role sees relevant menu
- Use vague menu labels - e.g., "Tools", "Manage"
- Mix internal links with external - Use separate sections
- Link to non-existent routes - Always validate target pages exist
- Update menu on every page load - Cache aggressively
- Forget to maintain menu order - Users expect consistent structure
- Bypass role checks - Always filter menu by user roles
Related Documentation
- Home - Main documentation index
- OBJECT_MANAGEMENT.md - Entity definitions
- PAGE_CONFIGURATION.md - Form pages linked from menu
- DATABASE.md - Core schema reference