Skip to main content

RBAC (Role-Based Access Control) System

This RBAC system allows you to control access to features (menus, context menus, actions, pages) based on user roles, persons, and organizations.

Architecture​

The RBAC system follows the same pattern as other @xond/ui initialization functions:

  1. Library (@xond/ui): Provides feature registry and access checking functions
  2. App: Initializes RBAC with API endpoints and syncs features to database
  3. Database: Stores features and access rules in config schema

Database Schema​

The RBAC system uses two tables in the config schema:

  • config.feature: Stores registered features (menus, context menus, actions, etc.)
  • config.feature_access: Controls who has access to which features

Field Mapping​

The library uses camelCase field names (featureKey, featureName, metadata), but the database follows Xond's standard naming convention (code, name, additional_info). The backend must map between these:

Library FieldDatabase FieldNotes
featureKeycodeUnique identifier for the feature
featureNamenameHuman-readable name
metadataadditional_infoJSONB field for additional data
category-Optional, not in ERD (can be ignored)

The backend endpoints handle this mapping automatically.

Setup​

1. Create Database Schema​

Create the database schema based on your ERD. The RBAC system requires:

  • config schema
  • config.feature table with fields: feature_id, code, name, description, feature_type, additional_info
  • config.feature_access table with fields: feature_access_id, feature_id, person_id, role_id, organization_id, access_type

Refer to your database ERD for the complete schema definition.

2. Initialize RBAC in Your App​

In your app's main.tsx (or initialization file):

import { initRbac, syncFeaturesToDatabase } from "@xond/ui";

// Initialize RBAC with API endpoints
initRbac(
"/rbac/sync-features", // Endpoint to sync features to database
"/rbac/check-access" // Endpoint to check user access
);

// Sync registered features to database (call after app initialization)
// This can be done periodically or on app startup
syncFeaturesToDatabase();

3. Create Backend Endpoints​

You need to create two endpoints in your API:

/rbac/sync-features (POST)​

Receives features from the frontend and upserts them to the database:

// Example endpoint handler
app.post("/rbac/sync-features", async (req, res) => {
const { features } = req.body;

// Upsert features to config.feature table
// Map library interface to Xond standard database schema:
// featureKey → code
// featureName → name
// metadata → additional_info
// category → ignore (optional, not in ERD)
for (const feature of features) {
await prisma.feature.upsert({
where: { code: feature.featureKey }, // Map featureKey → code
update: {
name: feature.featureName, // Map featureName → name
feature_type: feature.featureType,
description: feature.description,
additional_info: feature.metadata, // Map metadata → additional_info
last_updated: new Date(),
},
create: {
code: feature.featureKey, // Map featureKey → code
name: feature.featureName, // Map featureName → name
feature_type: feature.featureType,
description: feature.description,
additional_info: feature.metadata, // Map metadata → additional_info
},
});
}

res.json({ success: true, synced: features.length });
});

/rbac/check-access (GET or POST)​

Checks if the current user has access to a feature:

// GET /rbac/check-access?featureKey=menu.dashboard
// POST /rbac/check-access { featureKeys: ["menu.dashboard", "context.user.edit"] }

app.get("/rbac/check-access", async (req, res) => {
const { featureKey } = req.query; // Library sends featureKey
const userId = req.user?.id; // From JWT token
const roleId = req.user?.roleId;
const organizationId = req.user?.organizationId;

// First, find the feature by code (mapped from featureKey)
const feature = await prisma.feature.findUnique({
where: { code: featureKey }, // Map featureKey → code
});

if (!feature) {
// Feature doesn't exist, default to allow (open by default)
return res.json({ hasAccess: true });
}

// Query config.feature_access to check access
// Default: allow if no rules exist (open by default)
// Deny if explicit deny rule exists
// Allow if explicit allow rule exists

const hasAccess = await checkFeatureAccess(
feature.feature_id, // Use feature_id from database
userId,
roleId,
organizationId
);

res.json({ hasAccess });
});

Usage​

Registering Features​

Features are automatically registered when menus/context menus are created, or you can register them manually:

import { registerFeature, registerFeatures } from "@xond/ui";

// Register a single feature
registerFeature({
featureKey: "menu.dashboard",
featureName: "Dashboard",
featureType: "menu",
category: "navigation",
metadata: { href: "#/dashboard", icon: "HomeIcon" },
});

// Register multiple features
registerFeatures([
{
featureKey: "menu.users",
featureName: "Users",
featureType: "menu",
},
{
featureKey: "context.user.edit",
featureName: "Edit User",
featureType: "context_menu",
},
]);

Checking Access​

import { checkAccess, checkMultipleAccess, filterMenuItemsByAccess } from "@xond/ui";

// Check single feature access
const canAccessDashboard = await checkAccess("menu.dashboard");

// Check multiple features at once
const accessMap = await checkMultipleAccess(["menu.dashboard", "menu.users", "context.user.edit"]);

// Filter menu items by access
const filteredItems = await filterMenuItemsByAccess(menuItems);

Using with Components​

ContextMenu​

import { ContextMenu, IMenuItem } from "@xond/ui";
import { registerFeature } from "@xond/ui";

const menuItems: IMenuItem[] = [
{
menuTitle: "Edit",
menuAction: () => handleEdit(),
featureKey: "context.user.edit", // Add featureKey
},
{
menuTitle: "Delete",
menuAction: () => handleDelete(),
featureKey: "context.user.delete",
},
];

// Register features when menu is created
menuItems.forEach((item) => {
if (item.featureKey) {
registerFeature({
featureKey: item.featureKey,
featureName: item.menuTitle,
featureType: "context_menu",
});
}
});

// Filter by access before rendering
const filteredItems = await filterMenuItemsByAccess(menuItems);

<ContextMenu id="user-menu" menuItems={filteredItems} />;

SimpleSideNavigation​

import { SimpleSideNavigation, NavigationItem } from "@xond/ui";
import { registerFeature, filterMenuItemsByAccess } from "@xond/ui";

const navigation: NavigationItem[] = [
{
id: "dashboard",
name: "Dashboard",
href: "#/dashboard",
featureKey: "menu.dashboard", // Add featureKey
},
{
id: "users",
name: "Users",
href: "#/users",
featureKey: "menu.users",
},
];

// Register features
navigation.forEach((item) => {
if (item.featureKey) {
registerFeature({
featureKey: item.featureKey,
featureName: item.name || "",
featureType: "menu",
metadata: { href: item.href },
});
}
});

// Filter by access
const filteredNavigation = await filterMenuItemsByAccess(navigation);

<SimpleSideNavigation navigation={filteredNavigation} currentHash={currentHash} />;

Access Control Logic​

The access control follows this logic:

  1. Default Access: If no rules exist for a feature, access is allowed (open by default)
  2. Deny Rules: If a deny rule exists for the user/role/org, access is denied
  3. Allow Rules: If an allow rule exists (and no deny rule), access is allowed
  4. Priority: Deny rules take precedence over allow rules

Feature Key Naming Convention​

Use a hierarchical naming convention:

  • menu.{section} - Main navigation menus (e.g., menu.dashboard, menu.users)
  • context.{entity}.{action} - Context menu actions (e.g., context.user.edit, context.user.delete)
  • page.{page} - Page access (e.g., page.reports, page.settings)
  • action.{action} - General actions (e.g., action.export, action.print)

Best Practices​

  1. Register features early: Register features when menus/components are initialized
  2. Sync periodically: Sync features to database on app startup or periodically
  3. Cache access checks: Consider caching access checks to reduce API calls
  4. Fail secure: On error, deny access (fail secure) rather than allow
  5. Feature keys: Use consistent, hierarchical naming for feature keys

Example: Complete Integration​

// main.tsx
import { initRbac, syncFeaturesToDatabase } from "@xond/ui";

// Initialize RBAC
initRbac("/rbac/sync-features", "/rbac/check-access");

// After app initialization, sync features
setTimeout(() => {
syncFeaturesToDatabase();
}, 1000);

// In your navigation component
import { registerFeature, filterMenuItemsByAccess } from "@xond/ui";

const navigation = [
{ id: "dashboard", name: "Dashboard", href: "#/dashboard", featureKey: "menu.dashboard" },
// ... more items
];

// Register and filter
navigation.forEach((item) => {
if (item.featureKey) {
registerFeature({
featureKey: item.featureKey,
featureName: item.name || "",
featureType: "menu",
});
}
});

const filteredNav = await filterMenuItemsByAccess(navigation);