plugin-multi-tenant: an async `validate` override on the tenant field skips the plugin's required check (override not awaited)
Describe the Bug
@payloadcms/plugin-multi-tenant wraps the tenant field's validate (the overrides.validate a project may pass) in fieldValidation, which calls the override without awaiting it and then runs its own required check:
// packages/plugin-multi-tenant/src/fields/tenantField/index.ts (v3.88.0, lines 10–29)
const fieldValidation =
(validateFunction?: RelationshipFieldValidation): RelationshipFieldValidation =>
(value, options) => {
if (validateFunction) {
const result = validateFunction(value, options)
if (result !== true) {
return result
}
}
if (options.hasMany) { … } else {
if (!value) {
return options.req.t('validation:required')
}
}
return true
}When the override is async (any validate that looks something up, e.g. a membership check), validateFunction(...) returns a Promise. A Promise is !== true, so the wrapper returns the Promise itself. Payload awaits it; when it resolves true, the field is treated as valid — and the wrapper's own required check (the lines after) never runs.
Result: with an async override, an empty tenant value passes validation. On Postgres the insert then fails on the NOT NULL column with a 500 (null value in column "tenant_id" violates not-null constraint) instead of the expected 400 This field is required.; where the column is nullable, a row without a tenant is written.
Expected: the override is awaited and the plugin's required check still applies after it — i.e.
const fieldValidation =
(validateFunction?: RelationshipFieldValidation): RelationshipFieldValidation =>
async (value, options) => {
if (validateFunction) {
const result = await validateFunction(value, options)
if (result !== true) return result
}
…
}(Payload's validate already accepts a Promise, so awaiting is safe.)
Link to the code that reproduces this issue
https://github.com/payloadcms/payload/blob/v3.88.0/packages/plugin-multi-tenant/src/fields/tenantField/index.ts#L10-L29 — the wrapper itself; a config excerpt that triggers it is below (no repository needed: the defect is in the wrapper, any async override reaches it).
Reproduction Steps
- Configure the plugin with an async validate on the tenant field:
multiTenantPlugin({
tenantsSlug: 'tenants',
collections: { pages: {} },
tenantField: {
name: 'tenant',
validate: async (value, { req }) => {
// any async check — this one is a no-op
await Promise.resolve()
return true
},
},
})As a user who is a member of two tenants (so the plugin's cookie-less default resolves to nothing) send
POST /api/pageswith a body that omitstenant.Observe: no
This field is required.error. On Postgres the request answers 500 withnull value in column "tenant_id" ... violates not-null constraint; with a nullable column the page is created withtenant: null.Replace the override with a synchronous
validate: () => true(or remove it): the same request answers 400The following field is invalid: Tenant— the required check runs.
Which area(s) are affected? (Select all that apply)
plugin: multi-tenant
Environment Info
- Payload 3.88.0, @payloadcms/plugin-multi-tenant 3.88.0, @payloadcms/db-postgres 3.88.0
- Node 22.23.1
- macOS (reproduced locally against Postgres 17)
Source: payloadcms/payload