I was deep into building a multi-tenant SaaS platform with Payload CMS when users couldn't easily identify which tenant they were currently working with. The Payload admin interface handles tenant switching behind the scenes, but there was no visual indicator showing the active tenant, leading to confusion and costly mistakes when editing content for the wrong organization.
In this guide, I'll share how I solved this by implementing a cookie-based state management system and a visual Tenant Display component that keeps users context-aware at all times.
The Challenge with Payload CMS Multi-Tenant Visibility
When managing multiple tenants in Payload CMS, one of the most critical usability issues is ensuring administrators can clearly identify which organization's data they're currently working with. The built-in tenant switching functionality works silently in the background, but without visual confirmation, users can:
Accidentally edit content for wrong tenant
Spend time checking tenant context before making changes
Create content that's associated with incorrect organization
Experience cognitive load from constant context-switching
This problem becomes especially severe when:
Users manage 5+ tenants with similar names
Team members work across multiple tenant contexts
Critical updates need to be made quickly without errors
A visual tenant indicator is essential for preventing costly mistakes in multi-tenant SaaS applications.
The Solution: Cookie-Based State & Visual Components
The solution consists of three parts:
A backend endpoint to set the active tenant cookie when a user switches tenants.
A frontend component to read this cookie and display the tenant in the admin UI.
Middleware integration to ensure the state is consistent across the application.
Let's build this step by step.
1. The Backend: Setting the Tenant Cookie
First, we need a way to persist the user's selection. Storing it in the database feels like overkill for session data, so we'll use a cookie. This allows both the client (admin UI) and the server (middleware/API) to access the current tenant ID.
We'll create a custom endpoint in Payload that sets a payload-tenant cookie whenever a user selects a tenant.
Now, whenever a user switches tenants, we can call this endpoint to update the state.
2. The Frontend: Tenant Display Component
Next, we need a visual component in the admin dashboard to show which tenant is active. This component will read the cookie we just set and display the tenant's logo and name.
The Display Component
Here is the React component that fetches the tenant details and renders the banner:
typescript
// File: src/components/payload/custom/TenantDisplay.tsx'use client';
importReact, { useEffect, useState } from'react';
import { getAllTenants } from'@/payload/db/index';
import isMediaObject, { getFirstMedia } from'@/payload/utilities/images/isMediaObject';
import { PayloadImageClient } from'@/components/payload/images/payload-image-client';
import styles from'./TenantDisplay.module.scss';
/**
* Client Component: Displays the current tenant in the Payload admin dashboard
* Reads the payload-tenant cookie to determine the current tenant
* Updates when the tenant is switched
*/exportdefaultfunctionTenantDisplay() {
const [tenants, setTenants] = useState<any[]>([]);
const [currentTenantId, setCurrentTenantId] = useState<string | number | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Helper function to get the cookie valueconst getCookieValue = (name: string): string | null => {
if (typeofdocument === 'undefined') returnnull;
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
returnnull;
};
useEffect(() => {
constfetchTenants = async () => {
try {
// 1. Get all tenants (using a server action or API call)const allTenants = awaitgetAllTenants();
setTenants(allTenants);
// 2. Read the cookieconst tenantCookie = getCookieValue('payload-tenant');
setCurrentTenantId(tenantCookie);
} catch (err) {
console.error('Error fetching tenant data:', err);
setError('Failed to load tenant info');
} finally {
setLoading(false);
}
};
fetchTenants();
// Optional: Listen for cookie changes if you have a sophisticated event system// Or simply rely on page reloads/navigations which is common in Payload admin
}, []);
if (loading) returnnull;
// Find the active tenant objectconst activeTenant = tenants.find(t => t.id === currentTenantId || t.id === Number(currentTenantId));
if (!activeTenant) returnnull;
return (
<divclassName={styles.tenantBanner}><divclassName={styles.tenantContent}>
{activeTenant.logo && isMediaObject(activeTenant.logo) && (
<divclassName={styles.tenantLogo}><PayloadImageClientmedia={activeTenant.logo}width={24}height={24}alt={activeTenant.title}
/></div>
)}
<spanclassName={styles.tenantName}>{activeTenant.title}</span>
{/* Link to the public site for this tenant if applicable */}
<ahref={`http://${activeTenant.slug}.localhost:3000`}
target="_blank"rel="noopener noreferrer"className={styles.visitLink}
>
View Site →
</a></div></div>
);
}
The Helper Utility
To make the above component work, we need a way to fetch all tenants efficiently:
typescript
// File: src/payload/db/index.ts (or wherever you keep DB helpers)import { getPayload } from'payload';
import configPromise from'@payload-config';
import { unstable_cache } from'next/cache';
/**
* Fetches all tenants with caching.
* Uses unstable_cache to avoid hitting the DB on every render.
*/exportconstgetAllTenants = () => {
returnunstable_cache(
async () => {
const payload = awaitgetPayload({ config: configPromise });
const result = await payload.find({
collection: 'tenants',
limit: 100,
depth: 1, // Populate the logo relationoverrideAccess: true, // Bypass access control
});
return result.docs;
},
['all-tenants-cache-key'],
{
tags: ['tenants'],
revalidate: false,
},
)();
};
Styling
Add some clean styles to make it look native to the Payload admin:
Scoping Globals: Fetching global settings specific to the active tenant.
Preview Mode: Determining which tenant's content to preview.
Middleware Service: Routing requests based on the established session.
Conclusion
By implementing this custom TenantDisplay component backed by a robust cookie-based state management system, you provide users with clear, persistent visibility of their current tenant context.
This solution eliminates the common pitfalls of hardcoded tenant access while providing users with an intuitive interface. The component automatically displays tenant logos, names, and domain links in a clean, compact banner that fits naturally within the Payload admin interface.
You now have a robust foundation for identifying and managing active tenants across both your frontend UI and backend logic.