payloadcms / payloadcms/payload
plugin-multi-tenant: a refused save on the tenants collection clears the form in a production build (when no tenant is selected)
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 a production build, a refused save on the tenants collection clears the form the user just filled in. The server answers 400 correctly and the toast appears, but every value is gone and the create view is back to [Untitled].
It happens only when no tenant is selected in the tenant selector — the normal state for a user who has access to all tenants (a platform admin) and has not picked one. With a tenant selected, the same save keeps every value and marks the field.
next dev keeps the values, so this only shows up once the app is built.
Cause
TenantSelectionProviderClient (packages/plugin-multi-tenant/src/providers/TenantSelectionProvider/index.client.tsx):
React.useEffect(() => {
if (!initialValue) {
setTenant({ id: undefined, refresh: true })
}
}, [initialValue, setTenant])
setTenant is a useCallback with tenantOptions in its dependency list. Saving a document in the tenants collection runs syncTenants(), which calls setTenantOptions(result.tenantOptions) with a freshly parsed array, so setTenant gets a new identity and this effect runs again. With no tenant selected, initialValue is falsy, so it calls setTenant({ id: undefined, refresh: true }) → router.refresh().
On a refused save the user is still on the create view, so that refresh re-renders the route from the server and replaces the filled form with an empty one. The user's input is lost.
Observed request sequence (production build, no tenant selected):
POST /api/tenants?depth=0&fallback-locale=null → 400 (the refusal)
GET /api/tenants/populate-tenant-options → 200 (syncTenants)
GET /admin/collections/tenants/create?_rsc=… → 200 (router.refresh → form cleared)
With a tenant selected, the third line does not happen and the form survives.
Reproduction steps
templates/blankwith@payloadcms/plugin-multi-tenant, atenantscollection whose field always refuses:
and{ name: 'code', type: 'text', validate: (v) => (typeof v === 'string' && v.startsWith('ok') ? true : 'The code must start with "ok".') }multiTenantPlugin({ collections: { things: {} }, tenantsSlug: 'tenants', userHasAccessToAllTenants: () => true }).- Create two tenants, so the selector does not auto-select the only one.
next build && next start, sign in, and clear the tenant selection (delete thepayload-tenantcookie, or pick nothing).- Go to
/admin/collections/tenants/create, fill in name, slug andcode: bad-code, and save.
Expected: 400, the field marked, and the form still holding what was typed — which is what happens when a tenant is selected.
Actual: the toast shows, and name, slug and code are empty; the heading is back to [Untitled].
Suggested fix
A refused save should not trigger a router refresh. Either skip the effect when the selection is already undefined and nothing changed, or keep setTenant stable across a syncTenants() that returns the same option ids (compare by value before calling setTenantOptions, as the sibling effect on initialTenantOptions already does).
Environment
payload 3.88.0 · @payloadcms/plugin-multi-tenant 3.88.0 · @payloadcms/db-sqlite 3.88.0 · next 16.3.3 · react 19.2.6 · node 22 · macOS
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 in packages/plugin-multi-tenant/src/providers/TenantSelectionProvider/index.client.tsx, tracing the useEffect, setTenant dependencies, and syncTenants behavior. Reproduce with the templates/blank setup using next build && next start, no tenant selected, and a refused tenants save. Done means the 400 response still marks the field while preserving the entered form values without refreshing the create view.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nextjs, react, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 82/100