Tenant Switching
Guide to implementing tenant switching in multi-tenant SaaS applications.
Overview
Tenant switching allows users (typically super admins) to switch between different tenant contexts in a single-tenant-per-session model, or allows users assigned to multiple tenants to switch between them.
Current Implementation
Lager Guru currently implements a single-tenant-per-user model:
- Each user is assigned to exactly one tenant
- User's tenant is determined at login
- No UI switching needed (user always sees their tenant)
Future: Multi-Tenant User Support
For users assigned to multiple tenants, switching can be implemented:
1. Store Current Tenant in Session
// In AuthContext or TenantContext
const [currentTenantId, setCurrentTenantId] = useState<string | null>(null);
// Get user's tenants
const { data: tenantUsers } = await supabase
.from('tenant_users')
.select('tenant_id, role')
.eq('user_id', user.id);
// Set initial tenant (first one, or last used)
useEffect(() => {
if (tenantUsers && tenantUsers.length > 0) {
const lastUsed = localStorage.getItem('last_tenant_id');
const initialTenant = lastUsed && tenantUsers.find(tu => tu.tenant_id === lastUsed)
? lastUsed
: tenantUsers[0].tenant_id;
setCurrentTenantId(initialTenant);
}
}, [tenantUsers]);2. Tenant Selector Component
function TenantSelector() {
const { user } = useAuth();
const { currentTenantId, setCurrentTenantId } = useTenant();
const [tenants, setTenants] = useState([]);
useEffect(() => {
// Fetch user's tenants
const fetchTenants = async () => {
const { data: tenantUsers } = await supabase
.from('tenant_users')
.select('tenant_id, role, tenant:tenants(id, name)')
.eq('user_id', user.id);
setTenants(tenantUsers || []);
};
fetchTenants();
}, [user]);
const handleSwitch = (tenantId: string) => {
setCurrentTenantId(tenantId);
localStorage.setItem('last_tenant_id', tenantId);
// Optionally reload page to refresh all data
window.location.reload();
};
return (
<Select value={currentTenantId} onValueChange={handleSwitch}>
{tenants.map(tu => (
<SelectItem key={tu.tenant_id} value={tu.tenant_id}>
{tu.tenant.name} ({tu.role})
</SelectItem>
))}
</Select>
);
}3. Route Guards
Protect routes to ensure tenant context is set:
function TenantRouteGuard({ children }: { children: React.ReactNode }) {
const { currentTenantId } = useTenant();
const { user } = useAuth();
if (!currentTenantId) {
// User has no tenant assignment
return <Navigate to="/tenant-selection" />;
}
// Verify user has access to current tenant
const { data: hasAccess } = await supabase
.from('tenant_users')
.select('id')
.eq('user_id', user.id)
.eq('tenant_id', currentTenantId)
.maybeSingle();
if (!hasAccess) {
return <Navigate to="/tenant-selection" />;
}
return <>{children}</>;
}Subdomain-Based Routing
Alternative approach: Use subdomains for tenant identification.
Implementation
// lib/subdomainRouting.ts
export function detectTenantFromSubdomain(): string | null {
const hostname = window.location.hostname;
const parts = hostname.split('.');
if (parts.length >= 3) {
const subdomain = parts[0];
// Map subdomain to tenant_id
return getTenantIdFromSubdomain(subdomain);
}
return null;
}DNS Configuration
acme.lager-guru.com → Tenant: Acme Corp
example.lager-guru.com → Tenant: Example IncRoute Handling
// App.tsx
const tenantId = detectTenantFromSubdomain();
if (tenantId) {
// Set tenant context
setCurrentTenantId(tenantId);
} else {
// No subdomain, show tenant selection
return <TenantSelectionPage />;
}Tenant Context Provider
Create a context to manage tenant state:
// contexts/TenantContext.tsx
interface TenantContextType {
currentTenantId: string | null;
setCurrentTenantId: (id: string | null) => void;
currentTenant: Tenant | null;
userTenants: TenantUser[];
isTenantAdmin: boolean;
}
export const TenantProvider = ({ children }) => {
const { user } = useAuth();
const [currentTenantId, setCurrentTenantId] = useState<string | null>(null);
const [currentTenant, setCurrentTenant] = useState<Tenant | null>(null);
const [userTenants, setUserTenants] = useState<TenantUser[]>([]);
// Load user's tenants
useEffect(() => {
if (user) {
loadUserTenants();
}
}, [user]);
// Load current tenant details
useEffect(() => {
if (currentTenantId) {
loadTenantDetails(currentTenantId);
}
}, [currentTenantId]);
const isTenantAdmin = userTenants.find(
tu => tu.tenant_id === currentTenantId && tu.role === 'tenant_admin'
) !== undefined;
return (
<TenantContext.Provider value={{
currentTenantId,
setCurrentTenantId,
currentTenant,
userTenants,
isTenantAdmin,
}}>
{children}
</TenantContext.Provider>
);
};Automatic Tenant Injection
All queries automatically include tenant filtering via RLS, but you can also explicitly filter:
// lib/tenants.ts
export async function getTenantScopedQuery<T>(
table: string,
query: (client: SupabaseClient) => PostgrestQueryBuilder<T>
) {
const tenantId = await getCurrentUserTenantId();
if (!tenantId) {
throw new Error('No tenant context');
}
return query(supabase).eq('tenant_id', tenantId);
}
// Usage
const { data } = await getTenantScopedQuery('shipments', (client) =>
client.from('shipments').select('*')
);Security Considerations
1. Validate Tenant Access
Always verify user has access to tenant before switching:
async function switchTenant(tenantId: string) {
const { user } = useAuth();
// Verify access
const { data } = await supabase
.from('tenant_users')
.select('id')
.eq('user_id', user.id)
.eq('tenant_id', tenantId)
.maybeSingle();
if (!data) {
throw new Error('Access denied to this tenant');
}
// Switch tenant
setCurrentTenantId(tenantId);
}2. Clear Sensitive Data
When switching tenants, clear any cached data:
function switchTenant(tenantId: string) {
// Clear caches
queryClient.clear();
localStorage.removeItem('cached_data');
// Switch tenant
setCurrentTenantId(tenantId);
// Reload page to ensure clean state
window.location.reload();
}3. Audit Logging
Log tenant switches for security:
async function switchTenant(tenantId: string) {
// Log switch
await supabase.from('audit_logs').insert({
user_id: user.id,
action: 'tenant_switch',
details: { from_tenant: currentTenantId, to_tenant: tenantId },
tenant_id: tenantId,
});
// Perform switch
setCurrentTenantId(tenantId);
}UI Implementation
Tenant Switcher in Header
// components/layout/Header.tsx
function TenantSwitcher() {
const { currentTenantId, userTenants, setCurrentTenantId } = useTenant();
if (userTenants.length <= 1) {
// No switching needed
return null;
}
return (
<DropdownMenu>
<DropdownMenuTrigger>
{currentTenant?.name || 'Select Tenant'}
</DropdownMenuTrigger>
<DropdownMenuContent>
{userTenants.map(tu => (
<DropdownMenuItem
key={tu.tenant_id}
onClick={() => switchTenant(tu.tenant_id)}
>
{tu.tenant.name}
{tu.role === 'tenant_admin' && ' (Admin)'}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}Best Practices
- Single Tenant Per Session: For most use cases, one tenant per user session is sufficient
- Explicit Switching: If multi-tenant users are needed, make switching explicit and logged
- Clear State: Always clear caches when switching tenants
- RLS Enforcement: RLS policies automatically enforce tenant isolation
- UI Indicators: Clearly show which tenant the user is currently viewing
Related Documentation
- Auth Flow - How authentication determines tenant
- RLS Policies - How RLS enforces tenant isolation
- Database Structure - Tenant table structure