Multi-Tenant Architecture - Role-Based Access
Overview
The multi-tenant architecture uses role families to determine user type, not a separate isInleed boolean flag. This ensures consistency and maintainability.
Role System
Role Families
There are two role families defined in configs/roles.config.ts:
INLEED: {
inleedMaster: { lvl: 0, description: "Inleed user with full access" },
inleedSlave: { lvl: 1, description: "Inleed user with limited access" }
},
USER: {
commonMaster: { lvl: 2, description: "Common user with full access" },
commonSlave: { lvl: 3, description: "Common user with limited access" }
}
User Type Determination
- Inleed Users: Users with roles in the
INLEEDfamily (inleedMaster,inleedSlave) - Client Users: Users with roles in the
USERfamily (commonMaster,commonSlave)
Backend Implementation
Database Field: isInleed
The Credential model has an isInleed boolean field, but it's denormalized data computed from role:
// In credential.model.ts
isInleed: {
type: Boolean,
required: true,
default: false, // Defaults to client user
index: true,
}
Why keep this field?
- Performance: Avoids role family lookup on every request
- Database queries: Easier to query and index
- Middleware efficiency: Fast access in tenant isolation middleware
Important: This field should ALWAYS be computed from the role, never set manually.
Automatic Role-to-isInleed Mapping
When creating/updating credentials, isInleed is automatically set based on role:
// In credential.service.ts - createCredential
const inleedRoles = Object.keys(rolesList("INLEED"));
const isInleed = inleedRoles.includes(role);
const credential = await Credential.create({
// ... other fields
role,
isInleed, // Computed from role
});
Tenant Isolation Middleware
Uses req.isInleed (from user.isInleed) to determine tenant access rules:
// In tenantIsolation.middleware.ts
req.isInleed = user.isInleed || false;
if (req.isInleed) {
// Inleed users can override tenantId via query/header
req.tenantId = req.query.tenantId || req.headers['x-tenant-id'] || user.tenantId;
} else {
// Client users must use their credential's tenantId
req.tenantId = user.tenantId;
}
Frontend Implementation
Interface
The frontend ICredential interface includes isInleed since the backend returns it:
export interface ICredential {
_id: string;
tenantId?: string;
isInleed: boolean; // From backend, computed from role
role: string;
// ... other fields
}
Using Role-Based Checks
Best Practice: Use the useRoleAccess hook instead of directly checking user.isInleed:
import { useRoleAccess } from "~/hooks/useRoleAccess.hook";
function MyComponent() {
const { isInleedUser } = useRoleAccess();
if (isInleedUser) {
// Show Inleed-specific features
}
}
Why?
- Single source of truth: Role family check is consistent
- Flexibility: Easy to add more role families in the future
- Type safety: Hook provides computed values based on current user
useRoleAccess Hook
The hook provides convenient role checking utilities:
const {
// User info
user,
userRole,
userLevel,
// Role checks
hasRole,
hasAnyRole,
hasMinLevel,
isInFamily,
// Computed flags
isInleedUser, // Checks if role in INLEED family
isCommonUser, // Checks if role in USER family
isInleedMaster,
isInleedSlave,
isCommonMaster,
isCommonSlave,
} = useRoleAccess();
Implementation:
const isInleedUser = useMemo(() => {
return isInFamily("INLEED");
}, [user]);
This checks if the user's role belongs to the INLEED family by comparing against rolesList("INLEED").
Migration from isInleed to Role-Based
Backend Migration Script
The migration script sets isInleed based on role:
// In migrate-to-multitenant.ts
const MIGRATION_CONFIG = {
inleedRoles: ["inleedMaster", "inleedSlave"],
};
// Mark Inleed users
await Credential.updateMany(
{ role: { $in: MIGRATION_CONFIG.inleedRoles } },
{ $set: { isInleed: true } }
);
// Mark client users
await Credential.updateMany(
{ role: { $nin: MIGRATION_CONFIG.inleedRoles } },
{ $set: { isInleed: false } }
);
Frontend Components Updated
All components now use useRoleAccess hook:
- TenantGuard - Checks
isInleedUserbefore requiring tenant selection - TenantIndicator - Only shows for
isInleedUser - TenantSelectionPage - Redirects non-Inleed users
- UserContext - Uses
response.isInleedfor routing logic
Best Practices
When Creating New Credentials
Always compute isInleed from role:
// ✅ CORRECT
const inleedRoles = Object.keys(rolesList("INLEED"));
const isInleed = inleedRoles.includes(newUser.role);
await Credential.create({
userName,
email,
role,
isInleed, // Computed
});
// ❌ WRONG
await Credential.create({
userName,
email,
role: "inleedMaster",
isInleed: false, // NEVER hardcode or manually set!
});
When Checking User Type
Use role-based checks in frontend:
// ✅ CORRECT - Uses hook
const { isInleedUser } = useRoleAccess();
if (isInleedUser) { /* ... */ }
// ✅ ALSO CORRECT - Direct role check
const { isInFamily } = useRoleAccess();
if (isInFamily("INLEED")) { /* ... */ }
// ⚠️ WORKS but less flexible
if (user.isInleed) { /* ... */ }
When Adding New Roles
- Add role to appropriate family in
roles.config.ts - Backend will automatically compute
isInleedcorrectly - Frontend
useRoleAccesshook will work without changes
Summary
- Backend:
isInleedis a denormalized database field computed from role family - Frontend: Use
useRoleAccesshook for role-based checks - Source of Truth: Role family (
INLEEDvsUSER) - Benefit: Consistent, maintainable, and flexible architecture
The isInleed field exists for performance but should always reflect the role family membership.