---
title: "Active Tenant State Management & Admin Display in Payload CMS"
slug: "payload-cms-multi-tenant-state-management"
published: "2025-09-25"
updated: "2025-12-25"
validated: "2025-10-20"
categories:
  - "Payload"
tags:
  - "Payload CMS multi-tenant state"
  - "Next.js cookies tenant"
  - "payload tenant display"
  - "multi-tenant admin UI"
llm-intent: "reference"
audience-level: "intermediate"
framework-versions:
  - "payload cms"
  - "next.js"
  - "typescript"
  - "multi-tenant plugin"
status: "stable"
llm-purpose: "Learn to manage active tenant state in Payload CMS multi-tenant apps using Next.js cookies for persistent, tenant switching functionality."
llm-prereqs:
  - "Access to Payload CMS"
  - "Access to Next.js"
  - "Access to TypeScript"
  - "Access to Multi-tenant Plugin"
llm-outputs:
  - "Completed outcome: Learn to manage active tenant state in Payload CMS multi-tenant apps using Next.js cookies for persistent, tenant switching functionality."
---

**Summary Triples**
- (Active tenant state, is stored in, Next.js cookies to enable server-side and middleware access)
- (Next.js cookies, provide, persistence across page refreshes and availability inside middleware and route handlers)
- (Admin tenant switch component, updates, a dedicated cookie (e.g., active_tenant) via a secure route/handler)
- (Middleware, reads, the active tenant cookie and injects tenant id into request context for Payload resolution)
- (Cookie attributes, should include, httpOnly, secure (in prod), SameSite=strict/lax, and reasonable maxAge to protect session data)
- (Server-side routes/route handlers, must validate, that the requested tenant belongs to the authenticated user before setting the cookie)
- (Fallback behavior, uses, user default tenant or first available tenant when cookie is absent or invalid)
- (Payload CMS multi-tenant plugin, can be integrated, by resolving the tenant from cookie and injecting it into Payload's request context)
- (Testing strategy, includes, integration tests for middleware route handling and unit tests for admin switch actions)

### {GOAL}
Learn to manage active tenant state in Payload CMS multi-tenant apps using Next.js cookies for persistent, tenant switching functionality.

### {PREREQS}
- Access to Payload CMS
- Access to Next.js
- Access to TypeScript
- Access to Multi-tenant Plugin

### {STEPS}
1. Create getActiveTenant utility
2. Build setActiveTenant endpoint
3. Create admin interface component
4. Integrate with preview functionality
5. Add middleware integration

<!-- llm:goal="Learn to manage active tenant state in Payload CMS multi-tenant apps using Next.js cookies for persistent, tenant switching functionality." -->
<!-- llm:prereq="Access to Payload CMS" -->
<!-- llm:prereq="Access to Next.js" -->
<!-- llm:prereq="Access to TypeScript" -->
<!-- llm:prereq="Access to Multi-tenant Plugin" -->
<!-- llm:output="Completed outcome: Learn to manage active tenant state in Payload CMS multi-tenant apps using Next.js cookies for persistent, tenant switching functionality." -->

# Active Tenant State Management & Admin Display in Payload CMS
> Learn to manage active tenant state in Payload CMS multi-tenant apps using Next.js cookies for persistent, tenant switching functionality.
Matija Žiberna · 2025-09-25

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.

```typescript
// 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:

```typescript
// 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:

```typescript
// 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:

```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.
 */
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:

```scss
// 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.

```typescript
// 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:

```typescript
// 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](/blog/how-to-configure-globals-with-multi-tenant-plugin-in-payload-cms) using this state system.
> - Detailed setup for your [Local Development Environment](/blog/multi-tenant-dev-environment-nextjs-payload) to test these flows.

## LLM Response Snippet
```json
{
  "goal": "Learn to manage active tenant state in Payload CMS multi-tenant apps using Next.js cookies for persistent, tenant switching functionality.",
  "responses": [
    {
      "question": "What does the article \"Managing Active Tenant State in Payload CMS Multi-Tenant Applications with Next.js Cookies\" cover?",
      "answer": "Learn to manage active tenant state in Payload CMS multi-tenant apps using Next.js cookies for persistent, tenant switching functionality."
    }
  ]
}
```