payloadcms / payloadcms/payload
plugin-multi-tenant: single-tenant auto-select overrides the server's "no tenant selected" (fires for userHasAccessToAllTenants users)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 44.8k
- Forks
- 4.2k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 53
Description
Describe the Bug
In @payloadcms/plugin-multi-tenant, on an install with exactly one tenant, TenantSelectionProvider re-selects that tenant and writes payload-tenant on every page mount, ignoring an explicit "no tenant selected" from the server render.
The narrow claim: the initialTenantOptions sync effect writes a selection without consulting initialValue. A server render that deliberately resolved to no selection is overridden by the client on mount.
dist/providers/TenantSelectionProvider/index.client.js (3.86.0):
React.useEffect(() => {
if (initialTenantOptions.length > 0) {
setTenantOptions(/* … */)
if (initialTenantOptions.length === 1 && initialTenantOptions[0]) {
setSelectedTenantID(initialTenantOptions[0].value)
setTenantCookie({ value: String(initialTenantOptions[0].value) }) // ← no initialValue check
}
}
}, [initialTenantOptions])
Three more branches reach the same outcome, and all four are reachable only at tenantOptions.length === 1:
| Source | What it does |
|---|---|
providers/TenantSelectionProvider/index.js (~L28) |
initialValue = tenantOptions.length > 1 ? undefined : tenantOptions[0]?.value — the server pre-selects the only tenant even with no cookie |
index.client.js (~L161) |
the effect above — writes the cookie on every mount, ignoring initialValue |
index.client.js (~L179) |
initialValue && String(initialValue) !== getTenantCookie() → syncTenants() → writes it again, plus a /api/tenants/populate-tenant-options request on every admin page |
index.client.js (~L203) → setTenant (~L80) |
!initialValue → setTenant({ id: undefined }) → "if there is only one tenant, auto-select that tenant", refresh: true |
With two or more tenants none of this happens — the !initialValue effect clears the selection instead, which is the behaviour a caller would expect at one tenant too.
Why it matters: for a user with userHasAccessToAllTenants, "no tenant selected" is a meaningful state — it is the platform-wide view that is not any one tenant. On a one-tenant install that state cannot be held for longer than one navigation, including immediately after login.
Captured by hooking document.cookie's setter on a page that had no selection:
=== on /admin/platform/overview: cookie=1
WRITE payload-tenant=1; Max-Age=31536000; Path=/
at setTenantCookie (…)
at TenantSelectionProviderClient.useEffect (…)
Link to the code that reproduces this issue
n/a — reproduced against a stock install; the code paths are quoted above from @payloadcms/plugin-multi-tenant@3.86.0.
Reproduction Steps
- Payload 3.86.0 with
@payloadcms/plugin-multi-tenant,userHasAccessToAllTenantsreturning true for the logged-in user. - Seed exactly one tenant.
- Delete the
payload-tenantcookie and load any admin page. - Observe
payload-tenantwritten back immediately, from the effect above, with no user action — andinitialValuefrom the server disregarded. - Repeat with two tenants: the cookie is not written, and a stale one is cleared.
Which area(s) are affected? (Select all that apply)
plugin: multi-tenant
Environment Info
Payload: 3.86.0
@payloadcms/plugin-multi-tenant: 3.86.0
Next.js: 16.2.11
Node.js: 22.22.3
Suggested direction
Have the single-tenant auto-select respect an explicit "no selection": skip it when the server passed initialValue === undefined, or gate it on !userHasAccessToAllTenants — a user with access to every tenant is exactly the user for whom "no tenant" is meaningful.
Happy to open a PR if the maintainers agree on the shape.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with providers/TenantSelectionProvider/index.js and providers/TenantSelectionProvider/index.client.js, tracing initialValue, the initialTenantOptions effect, and setTenant. Reproduce with one tenant and userHasAccessToAllTenants, then compare with two tenants; done means an explicit no-selection server state remains unselected without an unexpected payload-tenant write or tenant-options request.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nextjs, react, typescript
- Domain
- backend, frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 65/100