Complete Developer Guide for Building Workflow Engines, Role-Based Access Control, IT Service Management, and Asset Management Systems
- Overview
- Technology Stack
- Visual Workflow Builder
- Role-Based Access Control (RBAC)
- IT Service Management (ITSM)
- Asset/Inventory Management
- System Integration Patterns
- Best Practices
This documentation outlines how to build enterprise-grade systems for:
- Visual Workflow Builder: Drag-and-drop interface for designing approval and fulfillment workflows
- Role-Based Access Control (RBAC): Hierarchical permission system with inheritance and caching
- IT Service Management (ITSM): Dynamic forms, requests, and automated permission management
- Asset/Inventory Management: Type-based inventory with state machine lifecycle
graph TD
classDef frontend fill:#0ea5e9,stroke:#0284c7,stroke-width:2px,color:#fff
classDef api fill:#64748b,stroke:#475569,stroke-width:2px,color:#fff
classDef logic fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
classDef data fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff
subgraph "Frontend Layer"
UI[React/Next.js UI]:::frontend
WorkflowEditor[Visual Workflow Editor]:::frontend
FormRenderer[Dynamic Form Renderer]:::frontend
end
subgraph "API Layer"
API[REST/GraphQL API]:::api
Middleware[Permission Middleware]:::api
Cache[Permission Cache]:::api
end
subgraph "Business Logic"
RBAC[RBAC Engine]:::logic
Workflow[Workflow Engine]:::logic
Jobs[Background Job Queue]:::logic
end
subgraph "Data Layer"
DB[("MongoDB/PostgreSQL")]:::data
Collections[Data Collections]:::data
end
UI --> API
WorkflowEditor --> API
FormRenderer --> API
API --> Middleware
Middleware --> Cache
Middleware --> RBAC
API --> Workflow
API --> Jobs
RBAC --> Collections
Collections --> DB
| Component | Recommended Technologies | Purpose |
|---|---|---|
| Frontend | React, Next.js, TypeScript | UI and Server-Side Rendering |
| Workflow Editor | ReactFlow, React-DnD | Visual drag-and-drop workflow design |
| Backend | Node.js, Payload CMS | API and CMS functionality |
| Database | MongoDB, PostgreSQL | Data persistence |
| Authentication | Azure AD, MSAL.js, NextAuth.js | SSO and identity management |
| Job Queue | Bull, Redis, Payload Jobs | Background task processing |
| Caching | In-memory cache, Redis | Permission caching |
| Styling | Tailwind CSS, shadcn/ui | Responsive and modern UI |
| Validation | Zod | Runtime type validation |
Enable non-technical users to define complex approval and fulfillment workflows through a visual interface.
graph LR
classDef startNode fill:#0ea5e9,stroke:#0284c7,stroke-width:2px,color:#fff
classDef approvalNode fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
classDef fulfillmentNode fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff
classDef configNode fill:#64748b,stroke:#475569,stroke-width:1px,color:#fff
subgraph "Workflow Canvas"
Start[Start Node]:::startNode --> A1[Approval Step]:::approvalNode
A1 --> A2[Approval Step]:::approvalNode
A2 --> F1[Fulfillment Step]:::fulfillmentNode
end
subgraph "Node Configuration"
Config[Step Properties Panel]:::configNode
Roles[Role Assignment]:::configNode
Rules[Approval Rules]:::configNode
end
subgraph "Data Storage"
JSON[JSON Graph Structure]:::configNode
end
| Node Type | Purpose | Configuration Options |
|---|---|---|
| Submitted | Entry point (auto-created) | Read-only, non-deletable |
| Approval | Requires approval before proceeding | Approver roles, min approvals, delegation |
| Fulfillment | Final action/completion step | Fulfiller roles |
// Workflow stored as JSON
interface WorkflowValue {
version: number;
nodes: StepNode[];
edges: Edge[];
}
interface StepNode {
id: string;
type: "submitted" | "approval" | "fulfillment";
data: StepData;
position: { x: number; y: number };
deletable?: boolean;
}
interface StepData {
label: string;
// For approval nodes
approvalType?: "role" | "lineManager" | "departmentHead";
approverRole1?: string;
approverRole2?: string;
// ... up to N approver roles
minApprovals?: number;
allowDelegate?: boolean;
delegateRole?: string;
// For fulfillment nodes
fulfillerRole1?: string;
fulfillerRole2?: string;
// ... up to N fulfiller roles
}
interface Edge {
id: string;
source: string; // Source node ID
target: string; // Target node ID
}Pre-built templates accelerate workflow creation:
| Template | Description | Use Case |
|---|---|---|
| Single Approval | Submit → Approval → Fulfillment | Simple requests |
| Two-Step Approval | Submit → Approval 1 → Approval 2 → Fulfillment | Hierarchical approval |
| Parallel Approval | Submit → [Approval A, Approval B] → Fulfillment | Multi-department sign-off |
flowchart TD
classDef step fill:#0ea5e9,stroke:#0284c7,stroke-width:2px,color:#fff
classDef decision fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
classDef storage fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff
A[Initialize Canvas]:::step --> B[Load ReactFlow]:::step
B --> C[Setup Node Types]:::step
C --> D[Create Node Components]:::step
D --> E[Handle Drag & Drop]:::step
E --> F[Manage Edges/Connections]:::step
F --> G[Persist to Database]:::storage
G --> H{On Form Submit}:::decision
H --> I[Serialize to JSON]:::step
I --> J[Store in Database]:::storage
// Initialize with ReactFlow
const [nodes, setNodes, onNodesChange] = useNodesState<StepData>(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
// Ensure "Submitted" node always exists
const ensureSubmitted = (workflow: WorkflowValue): WorkflowValue => {
const hasSubmitted = workflow.nodes.some((n) => n.id === "submitted");
if (!hasSubmitted) {
const submitted: StepNode = {
id: "submitted",
type: "submitted",
data: { label: "Submitted" },
position: { x: 50, y: 50 },
draggable: false,
deletable: false,
};
return { ...workflow, nodes: [submitted, ...workflow.nodes] };
}
return workflow;
};// Base node component with handles
const BaseNode = ({ label, color }: { label: string; color: string }) => (
<div style={{ borderColor: color }} className="node-container">
<Handle type="target" position={Position.Left} />
{label}
<Handle type="source" position={Position.Right} />
</div>
);
// Register node types
const nodeTypes: NodeTypes = {
submitted: (props) => <BaseNode label="Submitted" color="#0ea5e9" {...props} />,
approval: (props) => <BaseNode label={props.data?.label || "Approval"} color="#f59e0b" {...props} />,
fulfillment: (props) => <BaseNode label={props.data?.label || "Fulfillment"} color="#22c55e" {...props} />,
};// Drop handler for palette items
const onDrop = (event: React.DragEvent) => {
event.preventDefault();
const type = event.dataTransfer.getData("application/workflow-node");
const position = reactFlowInstance.screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
const newNode: StepNode = {
id: `${type}-${generateId()}`,
type: type as "approval" | "fulfillment",
position,
data: {
label: type === "approval" ? "Approval" : "Fulfillment",
approvalType: type === "approval" ? "role" : undefined,
},
};
setNodes((nodes) => [...nodes, newNode]);
};// Multi-role selection component
const MultiRoleSelect = ({
value,
options,
onChange
}: {
value: string[];
options: Role[];
onChange: (roles: string[]) => void;
}) => {
const toggle = (slug: string) => {
const next = value.includes(slug)
? value.filter(v => v !== slug)
: [...value, slug];
onChange(next);
};
return (
<div className="multi-select">
{options.map(role => (
<label key={role.id}>
<input
type="checkbox"
checked={value.includes(role.slug)}
onChange={() => toggle(role.slug)}
/>
{role.name}
</label>
))}
</div>
);
};
// Store roles as approverRole1, approverRole2, etc.
const writeApproverRoles = (data: StepData, selected: string[]): StepData => {
const next = { ...data };
// Clear existing
Object.keys(next).filter(k => k.startsWith('approverRole')).forEach(k => delete next[k]);
// Add new
selected.forEach((slug, idx) => {
next[`approverRole${idx + 1}`] = slug;
});
return next;
};// Serialize and save workflow
const saveWorkflow = async (formId: string, workflow: WorkflowValue) => {
const safeNodes = workflow.nodes.map((n) => ({
id: n.id,
type: n.type,
data: sanitizeNodeData(n.data),
position: { x: n.position.x, y: n.position.y },
}));
const safeEdges = workflow.edges.map((e) => ({
id: e.id,
source: e.source,
target: e.target,
}));
await fetch(`/api/forms/${formId}`, {
method: "PATCH",
body: JSON.stringify({
workflowBuilder: {
version: 1,
nodes: safeNodes,
edges: safeEdges,
},
}),
});
};Implement a flexible, hierarchical permission system that supports:
- Role inheritance
- Group ancestry
- Action implications
- Dynamic permission creation
- Performance-optimized caching
graph TB
classDef source fill:#64748b,stroke:#475569,stroke-width:2px,color:#fff
classDef engine fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
classDef agg fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff
classDef enforcement fill:#0ea5e9,stroke:#0284c7,stroke-width:2px,color:#fff
subgraph "Permission Sources"
DP[Direct Permissions]:::source
Roles[Role Assignments]:::source
Groups[Group Memberships]:::source
end
subgraph "Resolution Engine"
RI[Role Inheritance Chain]:::engine
GA[Group Ancestry Chain]:::engine
AGG[Permission Aggregator]:::agg
end
subgraph "Enforcement"
Cache[Permission Cache]:::enforcement
MW[Middleware]:::enforcement
AC[Access Control]:::enforcement
end
DP --> AGG
Roles --> RI --> AGG
Groups --> GA --> AGG
AGG --> Cache
Cache --> MW
Cache --> AC
Resources define what can be protected. They form a hierarchy.
interface PermissionResource {
id: string;
name: string; // "IT Service Management"
slug: string; // "itsm"
parent?: PermissionResource; // Parent resource for hierarchy
description?: string;
}
// Example resource hierarchy:
// system (root)
// ├── itsm
// │ ├── itsm-access (category)
// │ │ ├── access-card-form (form)
// │ │ └── visitor-pass-form (form)
// │ └── itsm-legal (category)
// └── documentsPermissions combine resources with actions.
interface Permission {
id: string;
name: string; // "ITSM Access - Manage"
slug: string; // "itsm-access.manage"
resource: PermissionResource;
action: Action;
category: "system" | "users" | "content" | "itsm" | "reports" | "settings";
level: "basic" | "standard" | "advanced" | "admin" | "super";
isSystem?: boolean; // Cannot be deleted
status: "active" | "inactive" | "deprecated";
}
type Action =
| "create"
| "read"
| "update"
| "delete" // CRUD
| "manage"
| "admin" // Meta-actions
| "approve"
| "fulfill"
| "delegate"; // Workflow-specificRoles are named permission sets with inheritance.
interface EmployeeRole {
id: string;
name: string; // "IT Manager"
slug: string; // "it-manager"
permissions: Permission[];
inheritsFrom?: EmployeeRole; // Parent role
level: number; // 1-100, higher = more authority
isSystem?: boolean;
status: "active" | "inactive";
}Groups organize users and can have permissions/roles.
interface EmployeeGroup {
id: string;
name: string; // "IT Department"
slug: string; // "it-department"
roles: EmployeeRole[];
permissions: Permission[]; // Direct group permissions
parentGroup?: EmployeeGroup;
}Higher-level actions automatically grant lower-level actions:
graph TD
classDef admin fill:#ef4444,stroke:#dc2626,stroke-width:2px,color:#fff
classDef manage fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
classDef workflow fill:#a855f7,stroke:#9333ea,stroke-width:2px,color:#fff
classDef crud fill:#64748b,stroke:#475569,stroke-width:1px,color:#fff
Admin[admin]:::admin --> Manage[manage]:::manage
Admin --> Approve[approve]:::workflow
Admin --> Fulfill[fulfill]:::workflow
Admin --> Delegate[delegate]:::workflow
Manage --> Create[create]:::crud
Manage --> Read[read]:::crud
Manage --> Update[update]:::crud
Manage --> Delete[delete]:::crud
function actionImplies(possessed: string, required: string): boolean {
const p = possessed.toLowerCase();
const r = required.toLowerCase();
if (p === r) return true;
if (p === "admin") return true; // Admin implies all
if (p === "manage") {
return ["create", "read", "update", "delete"].includes(r);
}
return false;
}Important
manage does NOT imply approve or fulfill. These workflow actions must be granted explicitly.
async function resolveEffectivePermissions(userId: string): Promise<{
permissions: string[];
isSuperadmin: boolean;
}> {
const permissions = new Set<string>();
let isSuperadmin = false;
const user = await getUser(userId);
// 1. Direct custom permissions
for (const perm of user.customPermissions) {
permissions.add(perm.slug);
}
// 2. Roles (with inheritance)
const allRoles = await resolveRoleInheritance(user.roles);
for (const role of allRoles) {
if (role.slug === "superadmin") isSuperadmin = true;
for (const perm of role.permissions) {
permissions.add(perm.slug);
}
}
// 3. Groups (with ancestry)
const allGroups = await resolveGroupAncestry(user.groups);
for (const group of allGroups) {
// Group's direct permissions
for (const perm of group.permissions) {
permissions.add(perm.slug);
}
// Group's roles
const groupRoles = await resolveRoleInheritance(group.roles);
for (const role of groupRoles) {
if (role.slug === "superadmin") isSuperadmin = true;
for (const perm of role.permissions) {
permissions.add(perm.slug);
}
}
}
return { permissions: Array.from(permissions), isSuperadmin };
}async function resolveRoleInheritance(roleIds: string[]): Promise<Role[]> {
const visited = new Set<string>();
const stack = [...roleIds];
const roles: Role[] = [];
while (stack.length > 0) {
const id = stack.pop()!;
if (visited.has(id)) continue; // Prevent cycles
visited.add(id);
const role = await getRoleById(id);
if (!role) continue;
roles.push(role);
if (role.inheritsFrom && !visited.has(role.inheritsFrom.id)) {
stack.push(role.inheritsFrom.id);
}
}
return roles;
}Performance optimization with in-memory caching:
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
interface CachedPermissions {
permissions: string[];
isSuperadmin: boolean;
timestamp: number;
}
const permissionCache = new Map<string, CachedPermissions>();
async function getEffectivePermissions(
userId: string,
): Promise<CachedPermissions> {
const cached = permissionCache.get(userId);
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
return cached;
}
const resolved = await resolveEffectivePermissions(userId);
const cacheEntry: CachedPermissions = {
...resolved,
timestamp: Date.now(),
};
permissionCache.set(userId, cacheEntry);
return cacheEntry;
}
function invalidateUserPermissions(userId: string): void {
permissionCache.delete(userId);
}
function invalidateAllPermissions(): void {
permissionCache.clear();
}// API route protection
export async function checkPermission(
request: Request,
requiredPermission?: string,
options?: { requireSuperadmin?: boolean },
): Promise<{ hasPermission: boolean; user: User | null }> {
const user = await getCurrentUser(request);
if (!user) return { hasPermission: false, user: null };
const { permissions, isSuperadmin } = await getEffectivePermissions(user.id);
// Superadmin bypass
if (isSuperadmin) {
return { hasPermission: true, user };
}
if (options?.requireSuperadmin) {
return { hasPermission: false, user };
}
if (!requiredPermission) {
return { hasPermission: true, user };
}
// Check with action implications
const [resource, action] = requiredPermission.split(".");
const hasPermission = permissions.some((perm) => {
const [permResource, permAction] = perm.split(".");
if (permResource !== resource) return false;
return actionImplies(permAction, action);
});
return { hasPermission, user };
}// Payload CMS collection access hooks
export const ITSMForms: CollectionConfig = {
slug: "itsm-forms",
access: {
read: async ({ req }) => {
const { isSuperadmin, permissions } = await getEffectivePermissions(
req.user.id,
);
if (isSuperadmin) return true;
// Return where clause for filtered access
return buildFormsReadWhere(permissions);
},
create: async ({ req, data }) => {
return await allowFormCreate(req, data);
},
update: async ({ req, id }) => {
return await allowFormUpdateOrDelete(req, id);
},
delete: async ({ req, id }) => {
return await allowFormUpdateOrDelete(req, id);
},
},
};// React hook for permission checks
function usePermissions() {
const [permissions, setPermissions] = useState<string[]>([]);
const [isSuperadmin, setIsSuperadmin] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchPermissions()
.then(({ permissions, isSuperadmin }) => {
setPermissions(permissions);
setIsSuperadmin(isSuperadmin);
})
.finally(() => setLoading(false));
}, []);
const hasPermission = useCallback((required: string): boolean => {
if (isSuperadmin) return true;
const [resource, action] = required.split('.');
return permissions.some(perm => {
const [permResource, permAction] = perm.split('.');
if (permResource !== resource) return false;
return actionImplies(permAction, action);
});
}, [permissions, isSuperadmin]);
return { hasPermission, isSuperadmin, loading, permissions };
}
// Usage
function AdminButton() {
const { hasPermission, loading } = usePermissions();
if (loading) return <Spinner />;
if (!hasPermission('documents.create')) return null;
return <button>Create Document</button>;
}Enable organizations to manage service requests through configurable forms, workflows, and automated permission management.
graph TB
classDef user fill:#0ea5e9,stroke:#0284c7,stroke-width:2px,color:#fff
classDef admin fill:#64748b,stroke:#475569,stroke-width:2px,color:#fff
classDef process fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
classDef auto fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff
subgraph "End User"
Dashboard[ITSM Dashboard]:::user
Submit[Submit Request]:::user
end
subgraph "Admin"
CatAdmin[Category Management]:::admin
FormAdmin[Form Builder]:::admin
WorkflowAdmin[Workflow Designer]:::admin
end
subgraph "Processing"
Requests[Request Queue]:::process
Approvers[Approver Assignment]:::process
Fulfillers[Fulfiller Assignment]:::process
end
subgraph "Automation"
PermJobs[Permission Jobs]:::auto
RoleGen[Role Generation]:::auto
end
Dashboard --> Submit
Submit --> Requests
CatAdmin --> FormAdmin
FormAdmin --> WorkflowAdmin
FormAdmin --> PermJobs
PermJobs --> RoleGen
Requests --> Approvers
Approvers --> Fulfillers
graph TD
classDef root fill:#0ea5e9,stroke:#0284c7,stroke-width:2px,color:#fff
classDef cat fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
classDef form fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff
ITSM[itsm<br/>Root]:::root --> Cat1[itsm-access<br/>Category]:::cat
ITSM --> Cat2[itsm-legal<br/>Category]:::cat
ITSM --> Cat3[itsm-transport<br/>Category]:::cat
Cat1 --> Form1[access-card-form<br/>Form]:::form
Cat1 --> Form2[visitor-pass-form<br/>Form]:::form
Cat2 --> Form3[contract-review-form<br/>Form]:::form
Cat3 --> Form4[vehicle-request-form<br/>Form]:::form
interface ITSMCategory {
id: string;
title: string; // "Access Management"
slug: string; // "access"
description?: string;
icon?: string; // Lucide icon name
enabled: boolean;
forms?: ITSMForm[]; // Related forms
}interface ITSMForm {
id: string;
title: string;
slug: string;
description?: string;
category: ITSMCategory;
icon?: string;
formType: "itsm" | "equipment";
inventoryType?: InventoryType; // For equipment forms
enabled: boolean;
removed: boolean;
// SLA Configuration
slaTime?: {
enabled: boolean;
duration: number;
unit: "hours" | "days";
};
// Form fields (dynamic)
fields: FormField[];
// Workflow definition
workflowBuilder: WorkflowValue;
}
interface FormField {
name: string;
type:
| "text"
| "textarea"
| "select"
| "checkbox"
| "date"
| "file"
| "relationship";
label: string;
required?: boolean;
options?: { label: string; value: string }[];
visibility?: ConditionalVisibility;
}
interface ConditionalVisibility {
enabled: boolean;
logic: "and" | "or";
rules: Array<{
field: string;
operator: "equals" | "notEquals" | "contains" | "isEmpty";
value: string;
}>;
}interface ITSMRequest {
id: string;
form: ITSMForm;
requester: Employee;
status: RequestStatus;
priority: "low" | "medium" | "high" | "urgent";
// Form data submitted
data: Record<string, any>;
// Workflow state
currentStep: string; // Current node ID in workflow
approvals: Approval[];
fulfillments: Fulfillment[];
// SLA tracking
slaDueAt?: Date;
slaBreached?: boolean;
// Timestamps
createdAt: Date;
updatedAt: Date;
completedAt?: Date;
}
type RequestStatus =
| "pending_approval"
| "approved"
| "rejected"
| "pending_fulfillment"
| "fulfilled"
| "cancelled";
interface Approval {
step: string;
approver: Employee;
action: "approved" | "rejected";
comments?: string;
timestamp: Date;
}When ITSM categories and forms are created, the system automatically generates permissions:
sequenceDiagram
autonumber
participant Admin
participant Collection
participant Hook
participant JobQueue
participant Database
Note over Admin,Database: Dynamic Permission Creation Flow
Admin->>Collection: Create Category/Form
Collection->>Hook: Trigger afterChange
Hook->>JobQueue: Queue permission job
rect rgb(241, 245, 249)
Note right of JobQueue: Background Processing
JobQueue->>Database: Create PermissionResource
JobQueue->>Database: Create Permissions (CRUD + manage/admin)
JobQueue->>Database: Create Roles (manager, approver, fulfiller)
JobQueue->>Database: Assign permissions to roles
end
async function handleCategoryPermissionCreation(
categorySlug: string,
categoryTitle: string,
) {
const resourceSlug = `itsm-${categorySlug}`;
// 1. Create permission resource
const resource = await upsertResource({
slug: resourceSlug,
name: categoryTitle,
parent: "itsm", // Parent is root ITSM
});
// 2. Create permissions
const actions = [
"create",
"read",
"update",
"delete",
"manage",
"approve",
"fulfill",
"admin",
];
for (const action of actions) {
await upsertPermission({
slug: `${resourceSlug}.${action}`,
name: `${categoryTitle} - ${capitalize(action)}`,
resource: resource.id,
action,
category: "itsm",
});
}
// 3. Create roles
const roleConfigs = [
{ suffix: "manager", label: "Manager", perms: ["manage"] },
{ suffix: "approver", label: "Approver", perms: ["read", "approve"] },
{ suffix: "fulfiller", label: "Fulfiller", perms: ["read", "fulfill"] },
{ suffix: "admin", label: "Admin", perms: ["admin"] },
];
for (const config of roleConfigs) {
const roleSlug = `${resourceSlug}-${config.suffix}`;
const permissions = config.perms.map((p) =>
getPermissionId(`${resourceSlug}.${p}`),
);
await upsertRole({
slug: roleSlug,
name: `${categoryTitle} ${config.label}`,
permissions,
});
}
}flowchart TD
classDef startNode fill:#64748b,stroke:#475569,stroke-width:2px,color:#fff
classDef decision fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
classDef allow fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff
classDef deny fill:#ef4444,stroke:#dc2626,stroke-width:2px,color:#fff
Start([User Action]):::startNode --> CheckSuper{Superadmin?}:::decision
CheckSuper -->|Yes| Allow[✓ Allow]:::allow
CheckSuper -->|No| CheckAction{Action Type?}:::decision
CheckAction -->|Read Categories| ReadCat{Any itsm-* permission?}:::decision
CheckAction -->|Create Category| CreateCat{itsm.manage or admin?}:::decision
CheckAction -->|Update Category| UpdateCat{Category manage/admin?}:::decision
CheckAction -->|Create Request| CreateReq{Authenticated user?}:::decision
CheckAction -->|Update Request| UpdateReq{approve/fulfill/manage?}:::decision
ReadCat -->|Yes| Allow
ReadCat -->|No| Deny[✗ Deny]:::deny
CreateCat -->|Yes| Allow
CreateCat -->|No| Deny
UpdateCat -->|Yes| Allow
UpdateCat -->|No| Deny
CreateReq -->|Yes| Allow
CreateReq -->|No| Deny
UpdateReq -->|Yes| Allow
UpdateReq -->|No| Deny
Track physical assets with:
- Dynamic asset types
- State machine lifecycle
- Blueprint annotations
- Request integration
graph TB
classDef config fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
classDef asset fill:#0ea5e9,stroke:#0284c7,stroke-width:2px,color:#fff
classDef integration fill:#10b981,stroke:#059669,stroke-width:2px,color:#fff
subgraph "Configuration"
Types[Inventory Types]:::config
Templates[Blueprint Templates]:::config
end
subgraph "Assets"
Items[Inventory Items]:::asset
History[Item History]:::asset
end
subgraph "Integration"
Requests[ITSM Requests]:::integration
Assignment[Asset Assignment]:::integration
end
Types --> Items
Templates --> Items
Items --> History
Requests --> Assignment
Assignment --> Items
Define types of assets (cars, laptops, equipment):
interface InventoryType {
id: string;
name: string; // "Company Vehicle"
slug: string; // "company-vehicle"
identifierLabel: string; // "License Plate"
blueprintTemplate?: Media; // Default blueprint image
description?: string;
validationRules?: {
pattern?: string; // Regex for identifier
minLength?: number;
maxLength?: number;
format?: "uppercase" | "lowercase";
};
}Individual asset instances:
interface InventoryItem {
id: string;
type: InventoryType;
identifier: string; // "ABC-1234" (license plate)
status: InventoryStatus;
blueprint?: Media; // Item-specific blueprint
// Flexible metadata
metadata?: Record<string, any>;
// Current assignment
assignedTo?: Employee;
assignedRequest?: ITSMRequest;
// Blueprint annotations
comments: BlueprintComment[];
// History
history: HistoryEntry[];
notes?: string;
createdAt: Date;
updatedAt: Date;
}
type InventoryStatus =
| "available"
| "occupied"
| "under_review"
| "under_repair"
| "reserved"
| "retired";
interface BlueprintComment {
id: string;
position: { x: number; y: number }; // Position on blueprint
text: string;
status: "open" | "resolved";
createdBy: Employee;
resolvedBy?: Employee;
createdAt: Date;
resolvedAt?: Date;
}stateDiagram-v2
classDef state fill:#0ea5e9,stroke:#0284c7,stroke-width:2px,color:#fff
classDef process fill:#f59e0b,stroke:#d97706,stroke-width:2px,color:#fff
classDef final fill:#ef4444,stroke:#dc2626,stroke-width:2px,color:#fff
[*] --> Available: Create Item
Available --> Occupied: Assign to Request
Available --> UnderRepair: Found Broken
Available --> Reserved: Reserve
Available --> Retired: Retire
Occupied --> UnderReview: Report Issue / Return
UnderReview --> UnderRepair: Needs Repair
UnderReview --> Reserved: Hold
UnderReview --> Retired: Unusable
UnderReview --> Available: Passed Review
UnderRepair --> Reserved: Repaired, Hold
UnderRepair --> Retired: Repair Failed
UnderRepair --> Available: Repair Complete
Reserved --> Retired: Cancel & Scrap
Reserved --> Available: Release
Retired --> Available: Restore to Service
| Current Status | Allowed Transitions | Notes |
|---|---|---|
| Available | Under Repair, Reserved, Retired | "Occupied" happens via assignment |
| Occupied | Under Review | Can only move to review |
| Under Review | Under Repair, Reserved, Retired, Available | Triage phase |
| Under Repair | Reserved, Retired, Available | Maintenance phase |
| Reserved | Retired, Available | Held for future use |
| Retired | Available | Can restore to service |
| Action | Allowed Status | Permission Required |
|---|---|---|
| Add Comments | Under Review | inventory-{type}.reviewer |
| Resolve Comments | Under Repair | inventory-{type}.maintainer |
| Change Status | Any | inventory-{type}.update |
| Delete Item | Any | inventory-{type}.admin |
When an item returns to "Available" status:
async function handleStatusChange(
item: InventoryItem,
newStatus: InventoryStatus,
) {
if (newStatus === "available") {
// 1. Snapshot current state
const snapshot = {
comments: item.comments,
assignedRequest: item.assignedRequest,
previousStatus: item.status,
timestamp: new Date(),
};
// 2. Save to history
await addToHistory(item.id, snapshot);
// 3. Clear current assignment and comments
await updateItem(item.id, {
status: "available",
comments: [],
assignedTo: null,
assignedRequest: null,
});
}
}When a request is submitted, the workflow engine processes it:
async function processRequest(request: ITSMRequest) {
const workflow = request.form.workflowBuilder;
const currentNode = findNode(workflow, request.currentStep);
if (!currentNode) return;
switch (currentNode.type) {
case "submitted":
// Move to first approval/fulfillment node
const nextEdge = workflow.edges.find((e) => e.source === currentNode.id);
if (nextEdge) {
await updateRequest(request.id, {
currentStep: nextEdge.target,
status: determineStatus(findNode(workflow, nextEdge.target)),
});
await notifyAssignees(request, nextEdge.target);
}
break;
case "approval":
// Check if all required approvals are met
const approvalNode = currentNode.data;
const minApprovals = approvalNode.minApprovals || 1;
const stepApprovals = request.approvals.filter(
(a) => a.step === currentNode.id,
);
if (stepApprovals.length >= minApprovals) {
await moveToNextStep(request, workflow, currentNode.id);
}
break;
case "fulfillment":
// Mark as pending fulfillment
await updateRequest(request.id, { status: "pending_fulfillment" });
await notifyFulfillers(request, currentNode);
break;
}
}Equipment forms can assign inventory items:
async function handleEquipmentRequest(request: ITSMRequest) {
if (request.form.formType !== "equipment") return;
const inventoryType = request.form.inventoryType;
// Find available item
const availableItem = await findAvailableItem(inventoryType.id);
if (availableItem) {
// Assign item to request
await updateItem(availableItem.id, {
status: "occupied",
assignedTo: request.requester,
assignedRequest: request.id,
});
// Update request with assigned item
await updateRequest(request.id, {
assignedInventory: availableItem.id,
});
}
}When roles/permissions change, invalidate caches:
// In role update hook
async function afterRoleUpdate(role: Role) {
// Find all users with this role
const usersWithRole = await findUsersWithRole(role.id);
// Invalidate their permission caches
for (const user of usersWithRole) {
invalidateUserPermissions(user.id);
}
// Also check groups with this role
const groupsWithRole = await findGroupsWithRole(role.id);
for (const group of groupsWithRole) {
const groupMembers = await getGroupMembers(group.id);
for (const member of groupMembers) {
invalidateUserPermissions(member.id);
}
}
}| Practice | Description |
|---|---|
| Principle of Least Privilege | Grant minimum permissions needed |
| Use Roles Over Direct Permissions | Easier to manage and audit |
| Superadmin Separation | Limit superadmin accounts, audit quarterly |
| Cache Invalidation | Clear cache when permissions change |
| Server-Side Enforcement | Never trust client-side permission checks alone |
| Practice | Description |
|---|---|
| Permission Caching | 5-minute TTL for permission resolution |
| Batch Queries | Resolve roles/groups in parallel |
| Shallow Hierarchies | Limit role/group nesting to 3-4 levels |
| Index Key Fields | Index slug, status, user identifiers |
| Background Jobs | Use job queues for permission creation |
| Practice | Description |
|---|---|
| Optimistic UI Visibility | Show elements when cache is cold |
| Clear Error Messages | Explain why access was denied |
| Self-Service | Let users see their own permissions |
| Admin Tools | Provide permission debugging for admins |
| Practice | Description |
|---|---|
| TypeScript | Use type safety for permission slugs |
| Document Permissions | Add descriptions to all permissions |
| Version Control | Track permission changes in migrations |
| Test Multiple Roles | Verify inheritance works correctly |
Setup:
- User:
john@company.com - Role:
itsm-access-manager - Permissions:
itsm-access.manage,itsm-access.approve
Capabilities:
- ✅ View "Access" category in admin
- ✅ Create/edit forms under "Access" category
- ✅ Approve requests for "Access" forms
- ✅ See all requests for "Access" forms
- ❌ Cannot create new categories
- ❌ Cannot access other categories
Setup:
- User:
sarah@company.com - Permission:
access-card-form.approve
Capabilities:
- ✅ See requests for "Access Card" form
- ✅ Approve/reject "Access Card" requests
- ❌ Cannot see form in admin panel
- ❌ Cannot edit the form
- ❌ Cannot fulfill requests
Setup:
- User:
admin@company.com - Permission:
itsm.admin
Capabilities:
- ✅ Full access to all ITSM categories
- ✅ Create/edit/delete categories and forms
- ✅ See and manage all ITSM requests
- ❌ Cannot access system collections (need
system.manage)
Setup:
- User:
tech@company.com - Role:
inventory-vehicle-maintainer - Permissions:
inventory-vehicle.read,inventory-vehicle.maintainer
Capabilities:
- ✅ View vehicle inventory items
- ✅ Resolve blueprint comments (mark repairs complete)
- ❌ Cannot add new comments (need
reviewer) - ❌ Cannot delete items (need
admin)
Building enterprise portal systems requires:
- Visual Workflow Builder: ReactFlow-based editor storing workflows as JSON graphs
- RBAC System: Hierarchical permissions with inheritance, implications, and caching
- ITSM Module: Dynamic forms with automatic permission/role generation
- Asset Management: Type-based inventory with state machine lifecycle
The architecture emphasizes:
- Flexibility: Dynamic configuration without code changes
- Security: Layered permission checking with server-side enforcement
- Performance: Caching and background job processing
- Maintainability: Clear separation of concerns and documented patterns
Document Version: 1.0
Last Updated: January 2026
Status: ✅ Complete