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