Multi-Tenant Back Office Implementation Guide
Overview
This document describes the Back Office (React) implementation for the multi-tenant architecture. The Back Office now supports two distinct user flows based on user type.
User Flows
Inleed User Flow (Internal Administrators)
1. Login → Authentication
2. Redirect to Tenant Selection Page
3. Select a tenant from paginated list
4. Redirect to Dashboard (scoped to selected tenant)
5. Can switch tenants at any time
Client User Flow (Regular Customers)
1. Login → Authentication
2. Direct redirect to Dashboard (scoped to their tenant)
3. Cannot access tenant selection
4. Cannot switch tenants
Architecture Components
1. Tenant Context
Location: Baldr-Bo/app/context/tenant.context.tsx
Purpose: Manages the currently selected tenant for Inleed users.
Key Features:
- Stores selected tenant in state and localStorage
- Persists tenant selection across page reloads
- Provides methods to switch tenants
- Automatically loads saved tenant on mount
API:
interface TenantContextType {
currentTenant: ITenant | null;
setCurrentTenant: (tenant: ITenant | null) => void;
refreshTenant: () => Promise<void>;
clearTenant: () => void;
}
Usage Example:
import { useTenant } from "~/context/tenant.context";
function MyComponent() {
const { currentTenant, setCurrentTenant } = useTenant();
// Check if tenant is selected
if (!currentTenant) {
return <div>Please select a tenant</div>;
}
// Use tenant ID for API calls
fetchData(currentTenant._id);
}
2. Updated User Context
Location: Baldr-Bo/app/context/user.context.tsx
Changes:
- Added
isInleedcheck to user object - Updated authentication redirect logic
- Inleed users → Tenant Selection Page
- Client users → Dashboard
New Behavior:
// After successful login
if (user.isInleed) {
navigate("/tenants/selection");
} else {
navigate("/dashboard");
}
3. Tenant Selection Page
Location: Baldr-Bo/app/pages/tenants/tenantSelection.page.tsx
Features:
- Paginated tenant list (default 20 per page)
- Search by name, slug, or email (debounced 500ms)
- Status indicators (active/suspended/inactive)
- Click to select tenant
- Only accessible by Inleed users
Component Structure:
export default function TenantSelectionPage() {
// Fetch tenants with pagination
const [tenants, setTenants] = useState<ITenant[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [searchQuery, setSearchQuery] = useState("");
// Handle tenant selection
const handleTenantSelect = (tenant: ITenant) => {
setCurrentTenant(tenant);
navigate("/dashboard");
};
return (
<DataTable
value={tenants}
paginator
lazy
// ... configuration
/>
);
}
4. Tenant API
Location: Baldr-Bo/app/api/tenant.api.ts
Methods:
class TenantApi {
// List all tenants (Inleed only)
async getAll(params?: {
page?: number;
limit?: number;
search?: string;
status?: TenantStatus;
}): Promise<ITenantListResponse>;
// Get tenant by ID
async getById(id: string): Promise<ITenant>;
// Create tenant (Inleed only)
async create(data: {...}): Promise<ITenant>;
// Update tenant (Inleed only)
async update(id: string, data: {...}): Promise<ITenant>;
// Delete tenant (Inleed only)
async delete(id: string): Promise<void>;
// Get tenant statistics
async getStats(id: string): Promise<{...}>;
}
5. Updated Credential Interface
Location: Baldr-Bo/app/interfaces/credential.interface.ts
New Fields:
export interface ICredential {
_id: string;
// Multi-tenant fields
tenantId?: string; // Present for client users
isInleed: boolean; // true for internal admins
// ... existing fields
}
Routing Configuration
Protected Routes
All routes except public ones require authentication. Add the tenant selection route:
// In routes configuration
const routes = [
// Public routes
{ path: "/connexion", element: <LoginPage /> },
{ path: "/mot-de-passe-oublie", element: <ForgotPasswordPage /> },
{ path: "/reinitialisation-mot-de-passe", element: <ResetPasswordPage /> },
// Tenant selection (Inleed only)
{ path: "/tenants/selection", element: <TenantSelectionPage /> },
// Protected routes
{ path: "/dashboard", element: <Dashboard /> },
// ... other routes
];
Route Guards
Implement route guards to protect tenant-specific routes:
function TenantGuard({ children }: { children: ReactNode }) {
const { user } = useUser();
const { currentTenant } = useTenant();
const navigate = useNavigate();
useEffect(() => {
// Inleed users must have selected a tenant
if (user?.isInleed && !currentTenant) {
navigate("/tenants/selection");
}
}, [user, currentTenant, navigate]);
return <>{children}</>;
}
// Usage
<Route path="/dashboard" element={
<TenantGuard>
<Dashboard />
</TenantGuard>
} />
API Call Patterns
For Inleed Users
When making API calls, Inleed users need to specify the tenant context:
// Option 1: Via query parameter (recommended)
const response = await fetch(
`/api/products?tenantId=${currentTenant._id}`,
{
headers: getAuthHeaders(),
}
);
// Option 2: Via header
const response = await fetch("/api/products", {
headers: {
...getAuthHeaders(),
"X-Tenant-Id": currentTenant._id,
},
});
For Client Users
Client users don't need to specify tenantId - it's automatically set by the middleware:
const response = await fetch("/api/products", {
headers: getAuthHeaders(),
});
Updating Base API Class
Update the BaseApi class to automatically include tenant context:
// In base.api.ts
import { useTenant } from "~/context/tenant.context";
class BaseApi<T> {
protected getHeaders() {
const headers: HeadersInit = {
"Content-Type": "application/json",
Authorization: `Bearer ${getToken()}`,
};
// Add tenant header if available
const currentTenant = localStorage.getItem("baldr_selected_tenant");
if (currentTenant) {
headers["X-Tenant-Id"] = currentTenant;
}
return headers;
}
}
UI Components
Tenant Indicator
Create a component to show current tenant in the header:
function TenantIndicator() {
const { user } = useUser();
const { currentTenant, clearTenant } = useTenant();
const navigate = useNavigate();
if (!user?.isInleed || !currentTenant) {
return null;
}
const handleChangeTenant = () => {
clearTenant();
navigate("/tenants/selection");
};
return (
<div className="flex items-center gap-2 p-2 bg-blue-50 rounded">
<i className="pi pi-building" />
<span className="font-semibold">{currentTenant.name}</span>
<Button
icon="pi pi-sync"
size="small"
text
onClick={handleChangeTenant}
tooltip="Switch tenant"
/>
</div>
);
}
Client User Dashboard
For client users, optionally show their tenant info:
function ClientDashboard() {
const { user } = useUser();
const [tenant, setTenant] = useState<ITenant | null>(null);
useEffect(() => {
if (user?.tenantId) {
TenantApi.getById(user.tenantId).then(setTenant);
}
}, [user]);
return (
<div>
<h1>Welcome to {tenant?.name}</h1>
{/* Dashboard content */}
</div>
);
}
Testing Multi-Tenant UX
Test Scenario 1: Inleed User Login
- Login with Inleed credentials
- Should redirect to
/tenants/selection - Should see paginated list of tenants
- Select a tenant
- Should redirect to
/dashboard - Should see tenant indicator in header
- All data should be scoped to selected tenant
- Click "Switch Tenant" → back to selection page
Test Scenario 2: Client User Login
- Login with client credentials
- Should redirect directly to
/dashboard - Should NOT see tenant selection page
- Should NOT see tenant switching option
- All data automatically scoped to their tenant
- Cannot access
/tenants/selection(auto-redirect)
Test Scenario 3: Tenant Persistence
- Login as Inleed user
- Select a tenant
- Refresh the page
- Should remain in the same tenant context
- Logout and login again
- Should still have the same tenant selected
Migration Checklist for Existing Components
For each existing component that fetches data:
- Check if user is Inleed
- If Inleed, ensure tenant is selected before fetching
- Include tenantId in API calls for Inleed users
- Handle "no tenant selected" state gracefully
- Update TypeScript interfaces if needed
Example Migration:
// Before
function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
ProductApi.getAll().then(setProducts);
}, []);
return <div>{/* ... */}</div>;
}
// After
function ProductList() {
const { user } = useUser();
const { currentTenant } = useTenant();
const [products, setProducts] = useState([]);
useEffect(() => {
// For Inleed users, ensure tenant is selected
if (user?.isInleed && !currentTenant) {
return; // Wait for tenant selection
}
ProductApi.getAll().then(setProducts);
}, [user, currentTenant]);
// Show message if Inleed user hasn't selected tenant
if (user?.isInleed && !currentTenant) {
return <div>Please select a tenant first</div>;
}
return <div>{/* ... */}</div>;
}
Security Considerations
Frontend Checks
- Route Protection: Ensure tenant selection page is only accessible by Inleed users
- UI Restrictions: Hide tenant switching UI for client users
- Data Display: Validate user permissions before showing sensitive data
Remember
⚠️ Frontend security is NOT sufficient. All security enforcement happens on the backend via middleware. Frontend checks are only for UX purposes.
Performance Optimization
Tenant Caching
// Cache tenant list for Inleed users
const [tenantCache, setTenantCache] = useState<Map<string, ITenant>>(new Map());
const getCachedTenant = async (id: string) => {
if (tenantCache.has(id)) {
return tenantCache.get(id)!;
}
const tenant = await TenantApi.getById(id);
setTenantCache(prev => new Map(prev).set(id, tenant));
return tenant;
};
Lazy Loading
Load tenant selection page only when needed:
const TenantSelectionPage = lazy(() =>
import("~/pages/tenants/tenantSelection.page")
);
Troubleshooting
Issue: Inleed user stuck in redirect loop
Solution: Clear localStorage and check authentication token
localStorage.removeItem("baldr_selected_tenant");
Issue: Client user can access tenant selection
Solution: Add route guard to check user.isInleed
Issue: API returns 403 for tenant data
Solution: Ensure X-Tenant-Id header is included in requests
Next Steps
- ✅ Create tenant context
- ✅ Build tenant selection page
- ✅ Update authentication flow
- ⚠️ Add tenant indicator to header
- ⚠️ Update all existing pages to handle tenant context
- ⚠️ Add route guards for tenant-specific pages
- ⚠️ Test both user flows thoroughly
- ⚠️ Add analytics for tenant usage
Additional Resources
- API Documentation:
/BaldrTs/documents/MULTI_TENANT_MIGRATION.md - User Context:
/Baldr-Bo/app/context/user.context.tsx - Tenant Context:
/Baldr-Bo/app/context/tenant.context.tsx