Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

Building Multi-Tenant Apps with Payload: End-to-end

Official Guide
Multi Tenant End to End
Multi Tenant End to End

Payload’s Multi-Tenant Plugin provides the fields, Admin UI behavior, and access-control infrastructure needed for tenant isolation—but your frontend still needs to resolve and query the correct tenant. Here's how to do that!

Awhile back, I put together a couple of examples showing how to build multi-tenant apps with Payload:

  • Path-based routing → /tenant-a/page
  • Domain-based routing → tenant-a.com

Both use Payload’s Multi-Tenant Plugin, but the biggest confusion I’ve seen isn’t installing the plugin—it’s understanding how everything fits together between the backend and the frontend.

This post walks through two approaches and, more importantly, explains the missing piece most people run into.

What the Multi-Tenant Plugin Actually Does

Payload’s Multi-Tenant Plugin provides the infrastructure for tenant isolation, not frontend routing.

Out of the box, it helps you:

  • Attach tenants to your collections
  • Scope queries by tenant
  • Apply tenant-aware access constraints to configured collections and users

But it does not:

  • Determine which tenant a frontend request belongs to
  • Handle domains or URLs
  • Configure your frontend

Minimal Setup (Backend)

Start with the official plugin:
https://payloadcms.com/docs/plugins/multi-tenant

A minimal config might look like:

1
import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant'
2
3
export default buildConfig({
4
plugins: [
5
multiTenantPlugin({
6
collections: {
7
pages: {},
8
},
9
}),
10
],
11
})

You’ll also need a tenants collection and to enable the plugin on any collection you want scoped.

Note: if you're using a custom collection name for your tenants (e.g. brands instead of tenants), pass tenantsSlug to the plugin:

1
multiTenantPlugin({
2
tenantsSlug: "brands",
3
collections: {
4
pages: {},
5
posts: {},
6
},
7
});

At this point, your Payload configuration is tenant-aware, but your frontend still does not know which tenant corresponds to the incoming URL.

The Missing Piece: Resolving the Tenant

This is where most people get stuck. Before you query Payload, you need to answer: “Which tenant is this request for?” This happens in your application’s routing layer—for example, in a route segment, layout, Next.js rewrite, middleware, or Proxy.

There are two common approaches:

Path-Based Routing

Example repo:
https://github.com/zubricks/path-based-multi-tenant

How it works

Your URL includes the tenant:

app.com/tenant-a/page

In Next.js, you might have:

1
// app/[tenant]/[slug]/page.tsx
2
export default async function Page({ params }) {
3
const { tenant: tenantSlug, slug } = await params;
4
5
// Step 1: resolve slug to a tenant record
6
const tenantResult = await payload.find({
7
collection: "tenants",
8
where: { slug: { equals: tenantSlug } },
9
limit: 1,
10
});
11
const tenant = tenantResult.docs[0];
12
if (!tenant) notFound();
13
14
// Step 2: query content filtered by tenant ID
15
const data = await payload.find({
16
collection: 'pages',
17
overrideAccess: false,
18
user,
19
where: {
20
and: [
21
{ tenant: { equals: tenant.id } },
22
{ slug: { equals: slug } },
23
],
24
},
25
})
26
return <RenderPage data={data} />;
27
}

Only include user when you actually have an authenticated Payload user. For public pages, overrideAccess: false applies your collection’s unauthenticated access rules, while the explicit tenant condition determines which tenant’s page is returned.

A good pattern is to validate the tenant exists in a layout component that wraps all [tenant] routes, so you don't repeat that check in every page.

Path based routing is simple to implement, does not require domain setup and works well locally for development and test.

Domain-Based Routing

Example repo:
https://github.com/zubricks/multi-tenant-example

How it works

Each tenant has its own domain:

tenant-a.com

tenant-b.com

One approach—and the one shown in Payload’s official documentation—is to use Next.js rewrites in next.config.js. The rewrite captures the hostname and passes it to a [tenantDomain] route segment.

1
async rewrites() {
2
return [
3
{
4
source: '/((?!admin|api)):path*',
5
destination: '/:tenantDomain/:path*',
6
has: [
7
{
8
type: 'host',
9
value: '(?<tenantDomain>.*)',
10
},
11
],
12
},
13
]
14
}

With this in place, a request to tenant-a.example.com/about is internally rewritten to /tenant-a.example.com/about, while the browser URL remains unchanged. Your [tenantDomain] segment receives the full hostname as its value.

Your layout or page then queries by the domain field on the tenant record:

1
// app/[tenantDomain]/layout.tsx
2
const { tenantDomain } = await params
3
4
const tenantResult = await payload.find({
5
collection: 'brands',
6
where: {
7
domain: { equals: tenantDomain },
8
},
9
limit: 1,
10
})

And when filtering content, you query by domain through the relationship:

1
payload.find({
2
collection: "pages",
3
where: {
4
and: [
5
{ "tenant.domain": { equals: tenantDomain } },
6
{ slug: { equals: slug } },
7
],
8
},
9
});

Common Pitfalls

“My data isn’t filtering”


Confirm that the frontend query includes an explicit tenant condition. If you expect Payload’s access control to provide additional filtering, remember that Local API calls bypass access control unless you set overrideAccess: false.

“All tenants see the same data”


Confirm that the collection is included in the plugin configuration, the query is constrained to the resolved tenant, and access control is enabled when the request depends on the current user’s tenant permissions.

“Frontend doesn’t match backend”


You’re resolving tenant one way (e.g. domain), but querying another way.

“It works locally but not in production”


This is often caused by domain, DNS, SSL certificate, hostname normalization, or rewrite-matching differences in production.

Final Thoughts

The Multi-Tenant Plugin provides the CMS infrastructure for tenant isolation, including tenant relationships, Admin filtering, and tenant-aware access constraints.

But a complete multi-tenant app requires:

  • Tenant-aware Payload configuration
  • Tenant-aware frontend queries
  • A routing strategy that reliably resolves each request to a tenant

If you keep that separation in mind, everything becomes much easier to reason about.