---
title: "Payload CMS Multi-Tenant Localization: Setup & Gotchas"
slug: "payload-multi-tenant-localization"
published: "2026-08-29"
updated: "2026-09-17"
categories:
  - "Payload"
tags:
  - "Payload CMS multi-tenant localization"
  - "Payload CMS localization"
  - "multi-tenant Payload CMS"
  - "filterAvailableLocales"
  - "defaultLocale crash"
  - "tenant-supported locales"
  - "multi-language websites"
  - "localizeStatus"
  - "admin locale selector"
  - "@payloadcms/plugin-multi-tenant"
  - "tenant locale configuration"
llm-intent: "reference"
audience-level: "intermediate"
framework-versions:
  - "payload@next"
  - "@payloadcms/plugin-multi-tenant@next"
  - "node@18+"
  - "typescript@5+"
status: "stable"
llm-purpose: "Payload CMS multi-tenant localization lets one codebase power sites with different languages—configure global and tenant locales and prevent admin panel…"
llm-prereqs:
  - "Access to Payload CMS"
  - "Access to @payloadcms/plugin-multi-tenant"
  - "Access to TypeScript"
  - "Access to Node.js"
llm-outputs:
  - "Completed outcome: Payload CMS multi-tenant localization lets one codebase power sites with different languages—configure global and tenant locales and prevent admin panel…"
---

**Summary Triples**
- (Localization architecture, approach, Keep a global superset of locales in the app and expose per-tenant subsets for each tenant to avoid showing every language to every editor)
- (Tenant locales, store, Maintain tenant.locales (array) in tenant config or a tenant global to represent supported locales per tenant)
- (Admin locale selector, filter, Use filterAvailableLocales (or equivalent admin hook) to limit admin locale dropdown to tenant.locales on each request)
- (Default-locale crash, cause, Admin can crash if the project's defaultLocale is not included in a tenant's locales)
- (Prevent defaultLocale crash, fix, Ensure tenant.defaultLocale is one of tenant.locales, or implement a runtime guard that falls back to the first tenant locale when defaultLocale is missing)
- (localizeStatus, use, Control which collections/fields are localized across tenants by configuring localizeStatus per-collection or field)
- (Implementation, pattern, Compute available locales server-side using tenant context (middleware/hooks) so both API responses and admin UI use the same per-tenant locale set)

### {GOAL}
Payload CMS multi-tenant localization lets one codebase power sites with different languages—configure global and tenant locales and prevent admin panel…

### {PREREQS}
- Access to Payload CMS
- Access to @payloadcms/plugin-multi-tenant
- Access to TypeScript
- Access to Node.js

### {STEPS}
1. Define all supported locales globally
2. Store tenant-specific locale lists
3. Filter admin locale selector per tenant
4. Protect the platform default locale
5. Localize only necessary fields
6. Separate UI visibility from authorization
7. Resolve tenant and locale on the frontend

<!-- llm:goal="Payload CMS multi-tenant localization lets one codebase power sites with different languages—configure global and tenant locales and prevent admin panel…" -->
<!-- llm:prereq="Access to Payload CMS" -->
<!-- llm:prereq="Access to @payloadcms/plugin-multi-tenant" -->
<!-- llm:prereq="Access to TypeScript" -->
<!-- llm:prereq="Access to Node.js" -->
<!-- llm:output="Completed outcome: Payload CMS multi-tenant localization lets one codebase power sites with different languages—configure global and tenant locales and prevent admin panel…" -->

# Payload CMS Multi-Tenant Localization: Setup & Gotchas
> Payload CMS multi-tenant localization lets one codebase power sites with different languages—configure global and tenant locales and prevent admin panel…
Matija Žiberna · 2026-08-29

> This guide is part of our comprehensive series on [Multi-Tenant Payload CMS & Next.js Architecture](/series/multi-tenant-payload-nextjs).

Multi-tenancy and localization are both well-supported concepts in Payload CMS. But an interesting architecture question comes up when you need to combine them:

**What if different tenants need different combinations of languages?**

For example, imagine one Payload installation powering several regional websites:

| Tenant / Website            | Languages                         |
| --------------------------- | --------------------------------- |
| Canada website              | English (Canada), French (Canada) |
| US website                  | English (US)                      |
| Future US bilingual website | English (US), Spanish (US)        |
| Another Canadian brand      | English (Canada), French (Canada) |
| Small campaign site         | English only                      |

We still want one Payload application and one shared codebase. We do **not** want every editor on every tenant to see every language supported anywhere in the system.

Fortunately, Payload's multi-tenant and localization features fit this architecture surprisingly well — with one sharp edge worth knowing about before you build on it, which I'll get to below.

If you haven't set up the multi-tenant plugin itself yet, start with our guide on [how to configure Globals with the multi-tenant plugin in Payload CMS](/blog/how-to-configure-globals-with-multi-tenant-plugin-in-payload-cms) — this article assumes tenants and the plugin are already wired up and focuses specifically on the localization layer on top. To handle locale-prefixed path migrations, check out [configuring redirects with the redirects plugin across multi-tenant, multi-locale sites](/blog/payload-redirects-plugin-multi-tenant-multi-locale-301s). For international search visibility and hreflang tags, read our [multi-tenant SEO guide](/blog/multi-tenant-seo-payload-nextjs-guide).

## The architecture

The easiest way to think about the setup is as three separate concerns:

```text
Payload application
│
├── Tenant
│   ├── Website A
│   ├── Website B
│   └── Website C
│
├── Localization
│   ├── en-CA
│   ├── fr-CA
│   ├── en-US
│   └── es-US
│
└── Tenant configuration
    ├── Website A → en-CA, fr-CA
    ├── Website B → en-US
    └── Website C → en-US, es-US
```

Payload knows about every locale that the overall platform can support.
Each tenant then defines which subset of those locales it actually uses.

This distinction is important.
We are **not creating a separate Payload configuration for every website**. We have one application-wide localization configuration and make the available languages tenant-aware.

## Step 1: Configure all supported locales globally

Payload Localization is configured at the application level.

A simplified configuration could look like this:

```ts
// File: payload.config.ts
import { buildConfig } from 'payload'

export default buildConfig({
  localization: {
    locales: [
      {
        label: 'English (Canada)',
        code: 'en-CA',
      },
      {
        label: 'French (Canada)',
        code: 'fr-CA',
      },
      {
        label: 'English (US)',
        code: 'en-US',
      },
      {
        label: 'Spanish (US)',
        code: 'es-US',
      },
    ],
    defaultLocale: 'en-CA',
  },
})
```

These are the locales that the **platform is capable of supporting**.
That does not mean every tenant needs to use all four.

Payload supports arbitrary locale codes, so using region-specific values such as `en-CA`, `fr-CA`, and `en-US` is perfectly reasonable.

One decision here matters more than it looks: whichever locale you set as `defaultLocale` needs to be supported by *every* tenant, or you need to handle the fallback carefully — I'll explain exactly why in Step 3, because it's a real crash, not a theoretical one.

## Step 2: Use Payload's multi-tenant model for the websites

Payload has an official multi-tenant plugin (`@payloadcms/plugin-multi-tenant`).

The plugin adds a tenant field to every collection you configure it against, adds a tenant selector to the Admin Panel so editors can switch between tenants, scopes list views and relationships to the active tenant, and cleans up a tenant's documents when that tenant is deleted. It also exposes a `getTenantAccess` utility and a per-collection `useTenantAccess` flag when you need custom access control — for example, shared media that every tenant should be able to read regardless of which tenant it belongs to.

Conceptually, our tenant collection might contain:

```ts
{
  name: 'Canada',
  slug: 'canada',
  domain: 'example.ca',
  supportedLocales: ['en-CA', 'fr-CA'],
  defaultLocale: 'en-CA',
}
```

The US tenant could instead contain:

```ts
{
  name: 'United States',
  slug: 'us',
  domain: 'example.com',
  supportedLocales: ['en-US'],
  defaultLocale: 'en-US',
}
```

And a future bilingual US site could use:

```ts
{
  name: 'US Bilingual',
  slug: 'us-bilingual',
  domain: 'another-example.com',
  supportedLocales: ['en-US', 'es-US'],
  defaultLocale: 'en-US',
}
```

The important design decision is this:

**Supported languages belong to the tenant's configuration.**

Avoid hardcoding assumptions such as:

```ts
if (tenant === 'canada') {
  return ['en-CA', 'fr-CA']
}
```

Instead, make `supportedLocales` data.

That makes adding another site or language combination a configuration change rather than an architecture change.

## Step 3: Filter Payload's locale selector by tenant

This is where Payload has a particularly useful feature.

Localization supports a `filterAvailableLocales` function. It runs server-side, receives the request and the complete list of locales, and lets you decide which languages should be visible in the Admin Panel's locale selector. Payload's own documentation specifically demonstrates using this to scope languages based on request headers in a multi-tenant application.

A simplified version could look something like this:

```ts
// File: payload.config.ts
localization: {
  defaultLocale: 'en-CA',
  locales: [
    { label: 'English (Canada)', code: 'en-CA' },
    { label: 'French (Canada)', code: 'fr-CA' },
    { label: 'English (US)', code: 'en-US' },
    { label: 'Spanish (US)', code: 'es-US' },
  ],
  filterAvailableLocales: async ({ req, locales }) => {
    const tenantID = getCurrentTenantID(req)
    if (!tenantID) {
      return locales
    }
    const tenant = await req.payload.findByID({
      collection: 'tenants',
      id: tenantID,
      req,
    })
    if (!tenant.supportedLocales?.length) {
      return locales
    }
    return locales.filter((locale) =>
      tenant.supportedLocales.includes(locale.code),
    )
  },
}
```

Now the experience becomes tenant-aware. When an editor selects the Canadian tenant, Payload can show:

```text
English (Canada)
French (Canada)
```

When they switch to the US tenant:

```text
English (US)
```

And another tenant could expose:

```text
English (US)
Spanish (US)
```

All of this still runs inside the same Payload application.

### The gotcha: filtering out the default locale crashes the Admin Panel

This is the sharp edge I flagged in Step 1, and it applies directly to the US tenant example above: the platform-wide `defaultLocale` is `en-CA`, but the US tenant's `supportedLocales` only contains `en-US`. If `filterAvailableLocales` filters the selector down to a list that excludes the config's `defaultLocale`, the Admin Panel crashes — this is a confirmed, open issue against Payload, not a hypothetical edge case.

There are two practical ways to avoid it. Either make sure `filterAvailableLocales` always includes the platform default regardless of the tenant's supported list (so `en-CA` stays selectable even on the US tenant, just not the one editors are expected to use), or, more robustly, treat the platform-wide `defaultLocale` as a value every tenant must include in `supportedLocales`, and validate that invariant when a tenant document is saved. The second option is more work up front but means a future tenant configured with a narrow locale list can't silently break the Admin Panel for its own editors.

It's also worth knowing that `filterAvailableLocales` doesn't automatically re-run on every navigation — its result is calculated at the root of the application rather than recomputed per page. If a tenant's `supportedLocales` changes while an editor is actively working, you may need to call `router.refresh()` from a component that watches for that change, rather than assuming the locale selector updates on its own.

## Localize only the fields that actually need translation

Payload localization works at the **field level**, which is useful for this architecture.

For example:

```ts
{
  name: 'title',
  type: 'text',
  localized: true,
}
```

Payload then stores a separate value for each locale.

This also works with more complex named field types, including arrays and blocks.

But that does not mean everything should automatically be localized.

Consider a hero:

```text
Hero
├── image
├── heading
├── description
├── CTA label
└── CTA destination
```

We may decide that the image is shared while the text is translated:

```ts
{
  name: 'hero',
  type: 'group',
  fields: [
    {
      name: 'image',
      type: 'upload',
      relationTo: 'media',
    },
    {
      name: 'heading',
      type: 'text',
      localized: true,
    },
    {
      name: 'description',
      type: 'textarea',
      localized: true,
    },
  ],
}
```

That allows:

```text
Same page
Same layout
Same image
English heading
French heading
English description
French description
```

For many structured websites, this is preferable to letting every locale have a completely independent page layout.

## Localization is different from Payload Admin language

There is another distinction worth understanding.

Payload has both **Localization** and **I18n**, but they solve different problems.

Localization controls the language of your **content**.
I18n controls the language of the **Payload Admin interface**.

An organization might therefore choose:

```text
Payload Admin interface
→ English
Website content
→ English
→ French
```

A French editor can work on French website content while still using an English CMS interface.

For international organizations whose internal working language is English, this can simplify training considerably.

## Do not confuse hiding locales with authorization

There is one architectural detail I would not overlook.

`filterAvailableLocales` is excellent for controlling which locales editors **see in the Admin UI**.

But UI filtering should not be your only security boundary.

If different users have different locale permissions, enforce those rules through access control as well.

For example:

```text
Translator A
Tenant: Canada
Allowed locale: fr-CA

Editor B
Tenant: Canada
Allowed locales: en-CA, fr-CA
```

The Admin UI can hide irrelevant locales, while access-control rules ensure a request cannot modify a locale the user is not authorized to edit.

The principle is the same as everywhere else in CMS security:

**UI visibility improves the experience. Access control provides the actual authorization.**

## What happens on the frontend?

The Payload multi-tenant plugin handles tenant-aware data inside Payload, but the frontend still needs to determine which tenant corresponds to the incoming request.

Payload's official multi-tenant guidance makes this distinction explicit: the plugin provides tenant isolation infrastructure, while your frontend resolves the tenant from the domain, subdomain, URL path, middleware, rewrite, or another routing mechanism.

For a domain-based architecture:

```text
example.ca
      ↓
resolve Canada tenant
      ↓
request locale fr-CA
      ↓
Payload
      ↓
Canada + fr-CA content
```

Meanwhile:

```text
example.com
      ↓
resolve US tenant
      ↓
request locale en-US
      ↓
Payload
      ↓
US + en-US content
```

The query therefore has two dimensions:

```text
Tenant + Locale
```

rather than simply:

```text
Locale
```

That distinction becomes extremely useful once several brands, markets and domains share the same backend.

## A practical mental model

I find this hierarchy useful:

```text
Payload
│
├── Tenant: Canada
│   ├── Domain: example.ca
│   ├── Default locale: en-CA
│   └── Locales
│       ├── en-CA
│       └── fr-CA
│
├── Tenant: United States
│   ├── Domain: example.com
│   ├── Default locale: en-US
│   └── Locales
│       └── en-US
│
└── Tenant: Future regional site
    ├── Domain: regional.example.com
    ├── Default locale: en-US
    └── Locales
        ├── en-US
        └── es-US
```

The application understands four locales.
No individual tenant needs to understand all four.

## Why this scales well

This becomes especially useful for organizations operating multiple brands and geographic markets.

You can start with:

```text
Brand A Canada
→ English + French
```

Then add:

```text
Brand A US
→ English
```

Later:

```text
Brand B Canada
→ English + French
```

And eventually:

```text
Brand A new regional market
→ English + Spanish
```

The fundamental Payload architecture does not need to change.
You add another tenant, define its domain and allowed locales, and reuse the same collections, workflows and application code where appropriate.

Individual brands can still have different templates, navigation, styling, permissions and frontend presentation.

Multi-tenancy answers:

**Which website does this content belong to?**

Localization answers:

**Which language version of that content do we want?**

Combining the two gives us:

```text
Website / Brand / Region
            +
        Language
            ↓
        Content
```

## One caveat: publishing status per locale

Payload also supports localized document status, which makes scenarios such as this possible:

```text
English
→ Published
French
→ Draft
```

That is useful when one translation is ready before another. It ships as an experimental flag, `localizeStatus`, enabled globally via `experimental: { localizeStatus: true }` or per-collection under `versions: { drafts: { localizeStatus: true } }`. When enabled, the internal `_status` field changes shape from a single string to a locale-keyed object, so if you're retrofitting it onto a collection that already has version history, expect to run Payload's migration helper to convert existing `_status` data rather than enabling the flag on a live collection and hoping for the best.

Because it's explicitly marked experimental, I'd test it thoroughly in a staging environment before making a production editorial workflow depend on it, and keep an eye on Payload's changelog for changes to the config shape. The underlying content localization functionality itself — the field-level `localized: true` behavior this whole article is built on — is much more established and carries none of that risk.

## Conclusion

If your requirement is:

> One Payload application powering multiple websites, where every website can have a different combination of languages,

Payload provides a very clean foundation for it.

Use the multi-tenant plugin to separate websites and brands. Configure the complete universe of locales globally. Store each tenant's `supportedLocales` and `defaultLocale`. Use `filterAvailableLocales` to make Payload Admin reflect the active tenant, while making sure the platform's default locale stays reachable so you don't hit the Admin Panel crash described above. Localize only the fields that actually need translation. And keep authorization separate from UI filtering when users have locale-specific permissions.

The result is a platform where:

```text
Canada → EN + FR
US → EN
Another market → EN + ES
Another brand → EN + FR
Campaign → EN only
```

can all coexist in **one Payload codebase and one CMS architecture**.

For multi-brand and multi-region platforms, that is a much cleaner model than creating a separate CMS instance for every combination of brand, domain and language.

## FAQ

**Does every tenant need to support the platform's `defaultLocale`?**
In practice, yes, or you need to actively guard against it. Filtering `filterAvailableLocales` down to a set that excludes the platform's `defaultLocale` crashes the Admin Panel — a confirmed Payload issue, not just a theoretical risk. Either keep the default reachable for every tenant or validate that invariant when tenant documents are saved.

**Can I give each tenant a completely independent set of locale codes with no overlap?**
Yes, as long as the platform-wide `defaultLocale` is handled per the caveat above. Locale codes are arbitrary strings in Payload, so `en-CA`, `en-US`, and `en-GB` can all coexist as distinct locales even though they represent the same underlying language.

**Does `filterAvailableLocales` update automatically when a tenant's supported locales change?**
Not automatically. Its result is computed at the root of the application rather than recalculated on every navigation, so a live change to a tenant's `supportedLocales` may need an explicit `router.refresh()` from a component watching for that change before the Admin UI reflects it.

**Should I use per-locale publishing status for all my localized content?**
Only after testing it in staging. `localizeStatus` is explicitly experimental as of this writing, requires a migration step if you're adding it to a collection with existing version history, and its config shape may still change. The standard field-level `localized: true` mechanism this article is built around does not carry that risk.

**Is this the same as running separate Payload instances per region?**
No, and that's the point. One Payload configuration and one deployed codebase serve every tenant; only the tenant document's `supportedLocales` and `defaultLocale` change between them. Adding a new market is a configuration change, not a new deployment.

Let me know in the comments if you have questions, and subscribe for more practical development guides.

Thanks, Matija

## LLM Response Snippet
```json
{
  "goal": "Payload CMS multi-tenant localization lets one codebase power sites with different languages—configure global and tenant locales and prevent admin panel…",
  "responses": [
    {
      "question": "What does the article \"Payload CMS Multi-Tenant Localization: Setup & Gotchas\" cover?",
      "answer": "Payload CMS multi-tenant localization lets one codebase power sites with different languages—configure global and tenant locales and prevent admin panel…"
    }
  ]
}
```