payloadcms / payloadcms/payload

plugin-multi-tenant: single-tenant auto-select overrides the server's "no tenant selected" (fires for userHasAccessToAllTenants users)

Open
#17,833 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

3.x payloadcms/payload plugin: multi-tenant
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) !initialValuesetTenant({ 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
  1. Payload 3.86.0 with @payloadcms/plugin-multi-tenant, userHasAccessToAllTenants returning true for the logged-in user.
  2. Seed exactly one tenant.
  3. Delete the payload-tenant cookie and load any admin page.
  4. Observe payload-tenant written back immediately, from the effect above, with no user action — and initialValue from the server disregarded.
  5. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.