Browse docs
Browse docs
How RBAC reduces real work inside Dashforge applications through declarative component APIs and built-in helpers.
Without Dashforge integration, you'd need to manually wrap every component with RBAC checks. Dashforge components accept an access prop that handles permissions declaratively.
Manual Approach (Without Dashforge)
function BookingForm() {
const { can } = useRbac();
const canEditSalary = can({ action: 'edit', resource: 'booking.salary' });
const canEditStatus = can({ action: 'edit', resource: 'booking.status' });
return (
<form>
{canEditSalary ? (
<TextField name="salary" label="Salary" />
) : null}
<TextField
name="status"
label="Status"
disabled={!canEditStatus}
/>
</form>
);
}Dashforge Approach (Declarative)
function BookingForm() {
return (
<DashForm>
<TextField
name="salary"
label="Salary"
access={{
action: 'edit',
resource: 'booking.salary',
onUnauthorized: 'hide',
}}
/>
<TextField
name="status"
label="Status"
access={{
action: 'edit',
resource: 'booking.status',
onUnauthorized: 'disable',
}}
/>
</DashForm>
);
}Dashforge form components support an access prop that accepts an AccessRequirement. The component handles permission evaluation internally.
<TextField
name="customerEmail"
label="Customer Email"
access={{
action: 'read',
resource: 'customer.email',
onUnauthorized: 'hide',
}}
/>hide — Component renders null when permission denied
<TextField
name="salary"
label="Salary"
access={{
action: 'read',
resource: 'booking.salary',
onUnauthorized: 'hide',
}}
/>disable — Component renders disabled when permission denied
<TextField
name="status"
label="Status"
access={{
action: 'edit',
resource: 'booking.status',
onUnauthorized: 'disable',
}}
/>readonly — Component renders read-only when permission denied
<TextField
name="createdAt"
label="Created At"
access={{
action: 'edit',
resource: 'booking.createdAt',
onUnauthorized: 'readonly',
}}
/>For actions and buttons, use useCan() directly to control visibility or disabled state.
Conditional action rendering
function BookingActions({ bookingId }: { bookingId: string }) {
return (
<Stack direction="row" spacing={2}>
<Button
access={{ action:'edit', resource:'booking', onUnauthorized:'hide'}}
onClick={() => editBooking(bookingId)}>
Edit Booking
</Button>
<Button
access={{action:'delete', resource:'booking', onUnauthorized:'hide'}}
color="error"
onClick={() => deleteBooking(bookingId)}
>
Delete Booking
</Button>
</Stack>
);
}Disabled state control
function CustomerActions({ customerId }: { customerId: string }) {
return (
<Button
access={{action:'delete', resource:'booking', onUnauthorized:'disabled'}}
onClick={() => archiveCustomer(customerId)}
>
Archive Customer
</Button>
);
}Resource-specific permissions
function InvoiceActions({ invoice }: { invoice: Invoice }) {
return (
<Button
access={{
action:'approve',
resource:'invoice',
resourceData:{ownerId:invoice.ownerId}
onUnauthorized:'disabled'
}}
onClick={() => approveInvoice(invoice.id)}
>
Approve Invoice
</Button>
);
}Using Hooks Outside Dashforge Components.
When you need permission checks outside of Dashforge form fields or button which custom components, page-level guards, derived state use these hooks directly.
useCan for single permission check.
The smple hook return a boolean for one access request.
import {useCan} from '@dashforge/rbac';
function ExportButton(){
const canExport = useCan({action:'export', resource:'booking'});
if (!canExport) return null;
return <button onClick={handleExport}>Export</button>;
}Use useRbac for Multiple checks in one compoment
return useCan(), evaluate(), and the current subject. More efficient than calling useCan multiple times because it avoids re-rendering on each call.
import {useRbac} from '@dashforge/rbac';
function BookingToolbar(){
const {can} = useRbac();
const canCreate = can({action:'create', resource:'booking'});
const canExport = can({action:'export', resource:'booking'});
const canDelete = can({action:'delete', resource:'booking'});
return (
<Stack direction="row" spacing={2}>
{canCreate && <Button onClick={handleCreate}}>New Boooking</Button>
{canExport && <Button onClick={handleExport}}>Export</Button>
{canDelete && <Button color="red" onClick={handleDelete}}>Delete</Button>
</Stack>
);
}evaluate() when you need the full decision with a reason string (e.g for audit logging or debug UI)
const {evaluate} = useRbac();
const decision = evaluate({action:'approve', resource:'invoice'});
// decision.granted -> boolean
// decision.reason -> string | undefinedsubject gives you the current user's id and roles. Useful for ownership checks or displaying role-specific copy:
const {subject, can} = useRbac();
const canEditOwn = can({
action:'edit',
resource:'booking',
resourceData:{ownerId:booking.ownerId}
});
return <span>Logged in as: {subject.id}</span>Can declarative alternative to useCanWhen you prefer JSX over an if statement:
import { Can} from '@dashforge/rbac';
function InvoicePage(){
return(
<div>
<InvoiceDetails />
<Can action="approve" resource="invoice" fallback={<p>No approve access.</p>}>
<ApproveButton/>
</Can>
</div>
)
}The fallback prop is optional, omit it and the children simply don't render when access is denied.
| Hook / Component | When to use |
|---|---|
useCan | One-off boolean check in a component |
useRbac | Multiple checks in one component, or need evaluate() / subject |
Can | Declarative JSX guard around a subtree |
access prop | On Dashforge components (Button, TextField, Select, etc.) |
Dashforge fields support both visibleWhen (UI/form logic) and access (permissions). Both must be satisfied for a field to be visible.
<TextField
name="otherReason"
label="Other Reason"
visibleWhen={(engine) =>
engine.getNode('category')?.value === 'other'
}
access={{
action: 'edit',
resource: 'booking.otherReason',
onUnauthorized: 'hide',
}}
/>
// Field is visible ONLY when:
// 1. category === 'other' (UI logic)
// 2. User has 'edit booking.otherReason' permission (RBAC)| Prop | Purpose |
|---|---|
visibleWhen | UI business logic (form state, field dependencies, conditional sections) |
access | Permissions (who can see/edit this field based on their role) |
Dashforge provides utilities for filtering navigation items and actions based on RBAC permissions.
import { filterNavigationItems } from '@dashforge/rbac';
function AppNav() {
const { can } = useRbac();
const allItems = [
{
id: 'bookings',
label: 'Bookings',
path: '/bookings',
access: { action: 'read', resource: 'booking' },
},
{
id: 'customers',
label: 'Customers',
path: '/customers',
access: { action: 'read', resource: 'customer' },
},
{
id: 'admin',
label: 'Admin',
path: '/admin',
access: { action: 'manage', resource: 'settings' },
},
];
const visibleItems = filterNavigationItems(allItems, can);
return <LeftNav items={visibleItems} />;
}import { filterActions } from '@dashforge/rbac';
function BookingToolbar() {
const { can } = useRbac();
const allActions = [
{
id: 'create',
label: 'Create Booking',
onClick: handleCreate,
access: { action: 'create', resource: 'booking' },
},
{
id: 'export',
label: 'Export',
onClick: handleExport,
access: { action: 'export', resource: 'booking' },
},
{
id: 'archive',
label: 'Archive All',
onClick: handleArchive,
access: { action: 'archive', resource: 'booking' },
},
];
const visibleActions = filterActions(allActions, can);
return <ActionMenu actions={visibleActions} />;
}A realistic admin workflow combining forms, actions, and navigation:
import { DashForm, TextField, Select } from '@dashforge/tw';
import { useRbac, filterNavigationItems } from '@dashforge/rbac';
function BookingManagementPage() {
const { can } = useRbac();
const canDeleteBooking = can({ action: 'delete', resource: 'booking' });
const canExport = can({ action: 'export', resource: 'booking' });
return (
<Stack spacing={4}>
{/* Form with access-controlled fields */}
<DashForm>
<TextField
name="customerName"
label="Customer Name"
access={{
action: 'read',
resource: 'booking.customerName',
onUnauthorized: 'hide',
}}
/>
<TextField
name="totalAmount"
label="Total Amount"
access={{
action: 'edit',
resource: 'booking.totalAmount',
onUnauthorized: 'readonly',
}}
/>
<Select
name="status"
label="Status"
options={['pending', 'confirmed', 'completed']}
access={{
action: 'edit',
resource: 'booking.status',
onUnauthorized: 'disable',
}}
/>
</DashForm>
{/* Actions controlled by RBAC */}
<Stack direction="row" spacing={2}>
{canDeleteBooking && (
<Button color="error" onClick={handleDelete}>
Delete Booking
</Button>
)}
{canExport && (
<Button onClick={handleExport}>
Export Data
</Button>
)}
</Stack>
</Stack>
);
}
// Navigation filtered by permissions
function AppLayout() {
const { can } = useRbac();
const allNavItems = [
{
id: 'bookings',
label: 'Bookings',
path: '/bookings',
access: { action: 'read', resource: 'booking' },
},
{
id: 'reports',
label: 'Reports',
path: '/reports',
access: { action: 'read', resource: 'reports' },
},
{
id: 'admin',
label: 'Admin',
path: '/admin',
access: { action: 'manage', resource: 'settings' },
},
];
const visibleNavItems = filterNavigationItems(allNavItems, can);
return <LeftNav items={visibleNavItems} />;
}Use RBAC for permissions, not UI logic
Don't use access to show/hide fields based on form state. Use visibleWhen for that. RBAC is for "who", not "when".
Keep policies centralized
Define your RbacPolicy in one place (e.g., config/rbac-policy.ts). Don't scatter role definitions across components.
Prefer component access prop when available
Use the access prop on form components instead of manual useCan() checks. It's more declarative and less code.
Avoid duplicating raw can() checks everywhere
If you find yourself writing the same can({ action, resource }) in multiple places, consider using filterActions or filterNavigationItems instead.
Keep resource naming consistent
Use a consistent naming convention for resources (e.g., booking.salary, customer.email). Establish a resource naming guide for your team.