• Home
BuildWithMatija
Get In Touch
  1. Home
  2. Blog
  3. Payload
  4. Active Tenant State Management & Admin Display in Payload CMS

Active Tenant State Management & Admin Display in Payload CMS

Implement robust active tenant switching with cookie-based state management

25th September 2025·Updated on:25th December 2025·MŽMatija Žiberna·
Payload
Active Tenant State Management & Admin Display in Payload CMS

📚 Comprehensive Payload CMS Guides

Detailed Payload guides with field configuration examples, custom components, and workflow optimization tips to speed up your CMS development process.

No spam. Unsubscribe anytime.

Related Posts:

  • •Multi-Tenant SEO with Payload & Next.js — Complete Guide
  • •How to Implement Payload CMS Multilingual Admin Interface: Complete Step-by-Step Guide
  • •Production-Ready Multi-Tenant Setup with Next.js & Payload

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:

  1. A backend endpoint to set the active tenant cookie when a user switches tenants.
  2. A frontend component to read this cookie and display the tenant in the admin UI.
  3. 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.

// File: src/endpoints/tenant/setTenant.ts
import { PayloadHandler } from 'payload'
import { cookies } from 'next/headers'

export const setTenant: PayloadHandler = async (req) => {
  const { tenantId } = await req.json()
  const cookieStore = await cookies()
  
  // Set the cookie with a long expiry
  cookieStore.set('payload-tenant', tenantId, {
    path: '/',
    maxAge: 60 * 60 * 24 * 30, // 30 days
    sameSite: 'lax',
    secure: process.env.NODE_ENV === 'production'
  })

  return Response.json({ success: true })
}

Register this endpoint in your Payload config:

// File: payload.config.ts
export default buildConfig({
  // ...
  endpoints: [
    {
      path: '/api/tenant/set',
      method: 'post',
      handler: setTenant,
    }
  ]
})

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:

// File: src/components/payload/custom/TenantDisplay.tsx
'use client';

import React, { 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
 */
export default function TenantDisplay() {
  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 value
  const getCookieValue = (name: string): string | null => {
    if (typeof document === 'undefined') return null;
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
    return null;
  };

  useEffect(() => {
    const fetchTenants = async () => {
      try {
        // 1. Get all tenants (using a server action or API call)
        const allTenants = await getAllTenants();
        setTenants(allTenants);

        // 2. Read the cookie
        const 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) return null;
  
  // Find the active tenant object
  const activeTenant = tenants.find(t => t.id === currentTenantId || t.id === Number(currentTenantId));
  
  if (!activeTenant) return null;

  return (
    <div className={styles.tenantBanner}>
      <div className={styles.tenantContent}>
        {activeTenant.logo && isMediaObject(activeTenant.logo) && (
           <div className={styles.tenantLogo}>
             <PayloadImageClient
               media={activeTenant.logo}
               width={24}
               height={24}
               alt={activeTenant.title}
             />
           </div>
        )}
        <span className={styles.tenantName}>{activeTenant.title}</span>
        
        {/* Link to the public site for this tenant if applicable */}
        <a 
          href={`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:

// 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.
 */
export const getAllTenants = () => {
  return unstable_cache(
    async () => {
      const payload = await getPayload({ config: configPromise });
      const result = await payload.find({
        collection: 'tenants',
        limit: 100,
        depth: 1, // Populate the logo relation
        overrideAccess: 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:

// File: src/components/payload/custom/TenantDisplay.module.scss
.tenantBanner {
  background-color: var(--theme-elevation-100);
  border-bottom: 1px solid var(--theme-elevation-200);
  padding: 0.5rem 1rem;
  width: 100%;
}

.tenantContent {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-size: 0.875rem;
  color: var(--theme-elevation-800);
}

.tenantLogo {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 4px;
  overflow: hidden;
  background: var(--theme-elevation-50);
  
  img {
    object-fit: contain;
  }
}

.tenantName {
  font-weight: 600;
}

.visitLink {
  margin-left: auto;
  font-size: 0.75rem;
  color: var(--theme-success-500);
  text-decoration: none;
  
  &:hover {
    text-decoration: underline;
  }
}

3. Integration with Payload Components

Finally, inject this component into your Payload admin UI. We usually place it in the global header or before the dashboard metrics.

// File: payload.config.ts
export default buildConfig({
  // ...
  admin: {
    components: {
      beforeDashboard: ['@/components/payload/custom/TenantDisplay'],
    },
  },
})

Integrating with Server-Side Logic

Having the cookie is great for the UI, but you also need to use it in your server-side logic, such as in API routes or Payload Hooks.

You can create a helper to retrieve the active tenant on the server:

// File: src/utilities/getActiveTenant.ts
import { cookies } from 'next/headers'

export async function getActiveTenant() {
  const cookieStore = await cookies()
  const tenantId = cookieStore.get('payload-tenant')?.value
  
  if (!tenantId) return null
  
  return tenantId
}

This helper becomes incredibly useful for:

  1. Scoping Globals: Fetching global settings specific to the active tenant.
  2. Preview Mode: Determining which tenant's content to preview.
  3. 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.

Next Steps:

  • Learn how to Configure Tenant-Specific Globals using this state system.
  • Detailed setup for your Local Development Environment to test these flows.
📄View markdown version
0

Comments

Leave a Comment

Your email will not be published

Stay updated! Get our weekly digest with the latest learnings on NextJS, React, AI, and web development tips delivered straight to your inbox.

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

You might be interested in

Multi-Tenant SEO with Payload & Next.js — Complete Guide
Multi-Tenant SEO with Payload & Next.js — Complete Guide

14th December 2025

How to Implement Payload CMS Multilingual Admin Interface: Complete Step-by-Step Guide
How to Implement Payload CMS Multilingual Admin Interface: Complete Step-by-Step Guide

26th September 2025

Production-Ready Multi-Tenant Setup with Next.js & Payload
Production-Ready Multi-Tenant Setup with Next.js & Payload

12th December 2025

Table of Contents

  • The Challenge with Payload CMS Multi-Tenant Visibility
  • The Solution: Cookie-Based State & Visual Components
  • 1. The Backend: Setting the Tenant Cookie
  • 2. The Frontend: Tenant Display Component
  • 3. Integration with Payload Components
  • Integrating with Server-Side Logic
  • Conclusion
On this page:
  • The Challenge with Payload CMS Multi-Tenant Visibility
  • The Solution: Cookie-Based State & Visual Components
  • Integrating with Server-Side Logic
  • Conclusion
Build With Matija Logo

Build with Matija

Matija Žiberna

Full Stack Developer specializing in Next.js and TypeScript. Co-founder of We Hate Copy Pasting, building solutions for D2C brands.

Quick Links

About
  • Projects
  • Commands
  • Blog
  • Contact
  • Get in Touch

    Have a project in mind? Let's discuss how we can help your business grow.

    Contact me →
    © 2026BuildWithMatija•Crafting digital experiences with code•All rights reserved