BuildWithMatija
  1. Home
  2. Blog
  3. Payload
  4. Payload CMS Multi-Tenant Localization: Setup & Gotchas

Payload CMS Multi-Tenant Localization: Setup & Gotchas

Configure per-tenant locales, filter the admin locale selector, and prevent default-locale crashes in Payload CMS.

29th August 2026·Updated on:31st August 2026··
Payload
Payload CMS Multi-Tenant Localization: Setup & Gotchas

Evaluating Payload CMS Implementation Costs?

Scope design, content structure, and migration hours to estimate a realistic production timeline and hosting setup.

Try the Cost EstimatorGet a Second Opinion

📚 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.

📄View markdown version
0

Frequently Asked Questions

About the author

Matija Žiberna

Matija Žiberna

Full-stack developer, co-founder

AboutResume

Self-taught full-stack developer sharing lessons from building software and startups.

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.

Contents

  • The architecture
  • Step 1: Configure all supported locales globally
  • Step 2: Use Payload's multi-tenant model for the websites
  • Step 3: Filter Payload's locale selector by tenant
  • The gotcha: filtering out the default locale crashes the Admin Panel
  • Localize only the fields that actually need translation
  • Localization is different from Payload Admin language
  • Do not confuse hiding locales with authorization
  • What happens on the frontend?
  • A practical mental model
  • Why this scales well
  • One caveat: publishing status per locale
  • Conclusion
  • FAQ
On this page:
  • The architecture
  • Step 1: Configure all supported locales globally
  • Step 2: Use Payload's multi-tenant model for the websites
  • Step 3: Filter Payload's locale selector by tenant
  • Localize only the fields that actually need translation
Build with Matija logo

Build with Matija

Senior-led B2B websites, applications, content systems, and digital infrastructure. Business-first, full-stack, AI-assisted, no handoffs.

Services

  • B2B Website Development
  • CMS Architecture Review & Platform Blueprint
  • Next.js + Payload Advisory
  • AI Integration & Implementation

Resources

  • CMS Hub
  • B2B Website Strategy
  • E-commerce Hub
  • Blog
  • Case Studies

Payload CMS

  • Payload CMS Developer
  • Payload CMS Migration
  • Payload CMS Demos
  • All Payload CMS Resources

Discuss your project

Planning a rebuild, migration, application, workflow change, or platform decision? Start with the business problem and the system behind it.

Book a discovery callContact me →
© 2026Build with Matija•All rights reserved•Privacy Policy•Terms of Service
BuildWithMatija
Get In Touch

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 / WebsiteLanguages
Canada websiteEnglish (Canada), French (Canada)
US websiteEnglish (US)
Future US bilingual websiteEnglish (US), Spanish (US)
Another Canadian brandEnglish (Canada), French (Canada)
Small campaign siteEnglish 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 the guide to configuring globals under the multi-tenant plugin — this article assumes tenants and the plugin are already wired up and focuses specifically on the localization layer on top.

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