Fixes https://github.com/payloadcms/payload/issues/14377
## Non-public errors were returned from delete and update endpoints
Previously, non-public errors (`500` errors, or errors without
`isPublic: true` and `status`) were returned from the delete and update
endpoint. For security reason, this PR excludes the error messages from
the response.
This aligns bulk endpoints with others (e.g., deleteByID) that already
gate error details through `routeError.ts`.
## Loosen up isPublic check when throwing API error
`APIError` now infers `isPublic` from the status code unless explicitly
provided:
- Non-500 (e.g., 4xx): `isPublic` defaults to `true`.
- 500 or missing status: `isPublic` defaults to `false`.
You can still override by passing a boolean `isPublic` explicitly.
Previously, the only way to mark the error as public was:
`throw new APIError('error message', 401, null ,false)`
which feels unnecessary explicit.
Now, you can do this:
`throw new APIError('error message', 401)`
which previously resulted in `isPublic: false` and now results in
`isPublic: true`.
## Stricter error check in routeError
`routeError` now uses `isErrorPublic(err, config)` to decide whether to
expose the error message. Compared to the old condition (`!config.debug
&& !err.isPublic && status === 500)`, this is a bit stricter and easier
to understand. Errors with `isPublic === false` are now hidden even for
non-500 statuses (previously they were shown).
- Adds `overrideAuth` to the MCP Plugin.
- Adds `MCPAccessSettings` as an exported type from the mcpPlugin
```ts
import { type MCPAccessSettings, mcpPlugin } from '@payloadcms/plugin-mcp'
// ... other config
plugins: [
mcpPlugin({
// ... other plugin config
overrideAuth: (req) => {
const { payload } = req
payload.logger.info('[Override MCP auth]:')
return {
posts: {
find: true,
},
products: {
find: true,
update: true,
},
} as MCPAccessSettings
},
})
]
```
### Custom Auth Behaviors
The bypassed system uses an API Key that contains `MCPAccessSettings`
information. The `overrideAuth` function will bypass the API Key system
and use your function when authorizing requests.
This means that your function must return a valid `MCPAccessSettings`
object to use.
Important: An intentional effort is being made during migration to not
modify runtime behavior. This implies that there will be several
assertions, non-null assertions, and @ts-expect-error. This philosophy
applies only to migrating old code to TypeScript strict, not to writing
new code. For a more detailed justification for this reasoning,
https://github.com/payloadcms/payload/pull/11840#discussion_r2021975897.
In this PR, instead of following the approach of migrating a subset of
files, I'm migrating all files by disabling a specific rule. In this
case, `strictNullChecks`.
`strictNullChecks` is a good rule to start the migration with because
it's easy to silence with non-null assertions or optional chainings.
Additionally, almost all ts strict errors are due to this rule.
This PR improves 200+ files, leaving only 68 remaining to migrate to
strict mode in the payload package.
Fixes https://github.com/payloadcms/payload/issues/11901
Previously, when `ValidationError` `errors.path` was referring to a
field with `label` defined as a function, the error message was
generated with `[object Object]`. Now, we call that function instead.
Since the `i18n` argument is required for `StaticLabel`, this PR
introduces so you can pass a partial `req` to `ValidationError` from
which we thread `req.i18n` to the label args.
<!--
Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.
Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as
possible:
- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Fixes #
-->
### What?
This PR adds a new error to be thrown when logging in while having
`verify: true` set but no email has been verified for the user yet.
### Why?
To have a more descriptive, actionable error thrown in this case as
opposed to the generic "Invalid email or password." This gives users
more insight into why the login failed.
### How?
Introducing a new error: `UnverifiedEmail` and adjusting the check to be
separate from `if (!user) { ... }`.
Fixes#11358
Notes:
- In terms of account enumeration: this should not be a concern here as
the check for throwing this error comes _after_ the check for valid args
as well as the find for the user. This means that credentials must be on
hand, both an email and password, before seeing this error.
- I have an int test written in `/test/auth/int.spec.ts` for this,
however whenever I try to commit it I get an error stating that the
`eslint@9.14.0` module was not found during `lint-staged`.
<details>
<summary>Int test</summary>
```ts
it('should respond with unverifiedEmail if email is unverified on
login', async () => {
await payload.create({
collection: publicUsersSlug,
data: {
email: 'user@example.com',
password: 'test',
},
})
const response = await restClient.POST(`/${publicUsersSlug}/login`, {
body: JSON.stringify({
email: 'user@example.com',
password: 'test',
}),
})
expect(response.status).toBe(403)
const responseData = await response.json()
expect(responseData.errors[0].message).toBe('Please verify your email
before logging in.')
})
```
</details>
Demo of toast:

### What?
Implement the
[typescript-strict-plugin](https://github.com/allegro/typescript-strict-plugin)
plugin in the payload (core) package.
### Why?
1. One strategy for incremental migration is to enable strictness rules
in tsconfig, fix some errors, and push them without committing the
changes to tsconfig.json. However, this is not feasible for a package as
large as Payload that has over 1000 typescript errors. Until the work is
done, new contributions would undo the work being done.
2. Even if no migration work is done after this PR, this change already
improves the strictness of the package. 89 of the 311 files within the
package already satisfy strict mode. This PR only adds a comment
`@ts-strict-ignore` to files that had at least one compilation error.
This way, the propagation of errors in those files is stopped.
3. New files created in the package are strict by default (this was the
main improvement in version 2 of `typescript-strict-plugin`).
I recommend starting the migration with this package because it is the
one that almost all the others depend on. Once we finish this package,
we can repeat the same strategy on another one, or use the strategy I
mentioned in point 1 if the package is small.
### Note
If you don't see errors in the IDE when you uncomment `//
@ts-strict-ignore`, try restarting the typescript server or VSCode
### How to contribute to the migration ❤️
1. Remove `// @ts-strict-ignore` comments from 1 or more files
2. Fix the pending errors (they should appear in your IDE's intellisense
or when running `cd packages/payload` + `pnpm build:types`
3. Submit your PR!
Important: You don't need to fix everything at once! Furthermore, I
recommend breaking this down into very small PRs to trace potential
issues later if there are any. So if you have 5 minutes, tackle a small
file—every bit counts! 🤗
Bumps the following dependencies:
- next
- typescript
- http-status
- nodemailer
- Payload & next versions in all templates
- Monorepo only: playwright and dotenv
Removes unused dependencies:
- ts-jest
- jest-environment-jsdom
- resend (we don't use their sdk, we only use their rest API)
### What?
Previously, field error messages displayed in toast notifications used
the field path to reference fields that failed validation. This
path-based approach was necessary to distinguish between fields that
might share the same name when nested inside arrays, groups, rows, or
collapsible fields.
However, the human readability of these paths was lacking, especially
for unnamed fields like rows and collapsible fields. For example:
- A text field inside a row could display as: `_index-0.text`
- A text field nested within multiple arrays could display as:
`items.0.subArray.0.text`
These outputs are technically correct but not user-friendly.
### Why?
While the previous format was helpful for pinpointing the specific field
that caused the validation error, it could be more user-friendly and
clearer to read. The goal is to maintain the same level of accuracy
while improving the readability for both developers and content editors.
### How?
To improve readability, the following changes were made:
1. Use Field Labels Instead of Field Paths:
- The ValidationError component now uses the label prop from the field
config (if available) instead of the field’s name.
- If a label is provided, it will be used in the error message.
- If no label exists, it will fall back to the field’s name.
2. Remove _index from Paths for Unnamed Fields (In the validationError
component only):
- For unnamed fields like rows and collapsibles, the _index prefix is
now stripped from the output to make it cleaner.
- Instead of `_index-0.text`, it now outputs just `Text`.
3. Reformat the Error Path for Readability:
- The error message format has been improved to be more human-readable,
showing the field hierarchy in a structured way with array indices
converted to 1-based numbers.
#### Example transformation:
##### Before:
The following fields are invalid: `items.0.subArray.0.text`
##### After:
The following fields are invalid: `Items 1 > SubArray 1 > Text`
Adds a feature to allow editors to schedule publish / unpublish events
in the future. Must be enabled by setting
`versions.drafts.schedulePublish: true` in your Collection / Global
configs.
https://github.com/user-attachments/assets/ca1d7a8b-946a-4eac-b911-c2177dbe3b1c
Todo:
- [x] Translate new i18n keys
- [x] Wire up locale-specific scheduled publish / unpublish actions
### What?
Allows configuration of the log level based on the error being thrown
and also downgrades common errors to be info instead of error by
default.
### Why?
Currently all errors result in logger.error being called which can
polute the logs with junk that is normal and doesn't need attention.
### How?
Adds a config property called `loggingLevels` that is used to override
the default log levels based on the name of the error being thrown.
Sanitize config will provide the defaulted 'info' level errors which can
be overriden in the config.
Before

After

Currently, Payload renders all custom components on initial compile of
the admin panel. This is problematic for two key reasons:
1. Custom components do not receive contextual data, i.e. fields do not
receive their field data, edit views do not receive their document data,
etc.
2. Components are unnecessarily rendered before they are used
This was initially required to support React Server Components within
the Payload Admin Panel for two key reasons:
1. Fields can be dynamically rendered within arrays, blocks, etc.
2. Documents can be recursively rendered within a "drawer" UI, i.e.
relationship fields
3. Payload supports server/client component composition
In order to achieve this, components need to be rendered on the server
and passed as "slots" to the client. Currently, the pattern for this is
to render custom server components in the "client config". Then when a
view or field is needed to be rendered, we first check the client config
for a "pre-rendered" component, otherwise render our client-side
fallback component.
But for the reasons listed above, this pattern doesn't exactly make
custom server components very useful within the Payload Admin Panel,
which is where this PR comes in. Now, instead of pre-rendering all
components on initial compile, we're able to render custom components
_on demand_, only as they are needed.
To achieve this, we've established [this
pattern](https://github.com/payloadcms/payload/pull/8481) of React
Server Functions in the Payload Admin Panel. With Server Functions, we
can iterate the Payload Config and return JSX through React's
`text/x-component` content-type. This means we're able to pass
contextual props to custom components, such as data for fields and
views.
## Breaking Changes
1. Add the following to your root layout file, typically located at
`(app)/(payload)/layout.tsx`:
```diff
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
+ import type { ServerFunctionClient } from 'payload'
import config from '@payload-config'
import { RootLayout } from '@payloadcms/next/layouts'
import { handleServerFunctions } from '@payloadcms/next/utilities'
import React from 'react'
import { importMap } from './admin/importMap.js'
import './custom.scss'
type Args = {
children: React.ReactNode
}
+ const serverFunctions: ServerFunctionClient = async function (args) {
+ 'use server'
+ return handleServerFunctions({
+ ...args,
+ config,
+ importMap,
+ })
+ }
const Layout = ({ children }: Args) => (
<RootLayout
config={config}
importMap={importMap}
+ serverFunctions={serverFunctions}
>
{children}
</RootLayout>
)
export default Layout
```
2. If you were previously posting to the `/api/form-state` endpoint, it
no longer exists. Instead, you'll need to invoke the `form-state` Server
Function, which can be done through the _new_ `getFormState` utility:
```diff
- import { getFormState } from '@payloadcms/ui'
- const { state } = await getFormState({
- apiRoute: '',
- body: {
- // ...
- },
- serverURL: ''
- })
+ const { getFormState } = useServerFunctions()
+
+ const { state } = await getFormState({
+ // ...
+ })
```
## Breaking Changes
```diff
- useFieldProps()
- useCellProps()
```
More details coming soon.
---------
Co-authored-by: Alessio Gravili <alessio@gravili.de>
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
Co-authored-by: James <james@trbl.design>
## Description
- Adds a new "join" field type to Payload and is supported by all database adapters
- The UI uses a table view for the new field
- `db-mongodb` changes relationships to be stored as ObjectIDs instead of strings (for now querying works using both types internally to the DB so no data migration should be necessary unless you're querying directly, see breaking changes for details
- Adds a reusable traverseFields utility to Payload to make it easier to work with nested fields, used internally and for plugin maintainers
```ts
export const Categories: CollectionConfig = {
slug: 'categories',
fields: [
{
name: 'relatedPosts',
type: 'join',
collection: 'posts',
on: 'category',
}
]
}
```
BREAKING CHANGES:
All mongodb relationship and upload values will be stored as MongoDB ObjectIDs instead of strings going forward. If you have existing data and you are querying data directly, outside of Payload's APIs, you get different results. For example, a `contains` query will no longer works given a partial ID of a relationship since the ObjectID requires the whole identifier to work.
---------
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
Co-authored-by: James <james@trbl.design>
- Adds `overrideLock` flag to `update` & `delete` operations
- Instead of throwing an `APIError` (500) when trying to update / delete
a locked document - now throw a `Locked` (423) error status
Allows username to be optional when using the new loginWithUsername
feature. This can be done by the following:
```ts
auth: {
loginWithUsername: {
requireUsername: false, // <-- new property, default true
requireEmail: false, // default: false
allowEmailLogin: true, // default false
},
},
```
We now validate the names of the field against an array of protected
field names.
Also added JSDoc since we can't enforce type strictness yet if `string |
const[]` as it always evaluates to `string`.
```
The name of the field. Must be alphanumeric and cannot contain ' . '
Must not be one of protected field names: ['__v', 'salt', 'hash', 'file']
@link — [https://payloadcms.com/docs/fields/overview#field-names](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)
```
## Description
Adds `loginWithUsername` option to auth config. When set to true, it
will inject an `username` field into the collection config which
replaces the `email` field in the UI. The `email` field is still
required but not unique.
The `username` field can be extended by passing a field named `username`
to your auth collection. Anything added to this field will be combined
with the initial field.
- [X] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.
## Type of change
- [X] New feature (non-breaking change which adds functionality)
## Checklist:
- [X] Existing test suite passes locally with my changes
---------
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
BREAKING: `ValidationError` now requires the `global` or `collection`
slug, as well as an `errors` property. The actual errors are no longer
at the top-level.
**BREAKING:**
- Narrows the type of the `plugins` prop of lexical features. Client props are now also automatically provided to the plugin components. To migrate, type your plugin as either `PluginComponent` or PluginComponentWithAnchor.
- `BlockQuoteFeature` has been renamed to `BlockquoteFeature`
- `createClientComponent` is now exported only from /components
- The `LexicalBlocks` and `FieldWithRichTextRequiredEditor` types have been removed in favor of just `Blocks` & `Fields`, as well as improved validation.