@payloadcms/codemod 4.0.0-internal.44ac9c8 → 4.0.0-internal.4a6b4c3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +6 -2
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.help.d.ts +4 -0
  4. package/dist/cli.help.d.ts.map +1 -0
  5. package/dist/cli.help.js +29 -0
  6. package/dist/cli.help.js.map +1 -0
  7. package/dist/cli.js +8 -0
  8. package/dist/cli.js.map +1 -1
  9. package/dist/cli.parseFlags.d.ts +1 -0
  10. package/dist/cli.parseFlags.d.ts.map +1 -1
  11. package/dist/cli.parseFlags.js +6 -0
  12. package/dist/cli.parseFlags.js.map +1 -1
  13. package/dist/registry.d.ts.map +1 -1
  14. package/dist/registry.js +6 -0
  15. package/dist/registry.js.map +1 -1
  16. package/dist/runbook/payload-v4-upgrade.md +2 -2
  17. package/dist/runbook/v4.mdx +370 -80
  18. package/dist/transforms/migrate-admin-context-properties/basic.input.js +32 -0
  19. package/dist/transforms/migrate-admin-context-properties/basic.input.js.map +1 -0
  20. package/dist/transforms/migrate-admin-context-properties/basic.output.js +30 -0
  21. package/dist/transforms/migrate-admin-context-properties/basic.output.js.map +1 -0
  22. package/dist/transforms/migrate-admin-context-properties/index.d.ts +3 -0
  23. package/dist/transforms/migrate-admin-context-properties/index.d.ts.map +1 -0
  24. package/dist/transforms/migrate-admin-context-properties/index.js +302 -0
  25. package/dist/transforms/migrate-admin-context-properties/index.js.map +1 -0
  26. package/dist/transforms/migrate-admin-context-properties/no-match.input.js +30 -0
  27. package/dist/transforms/migrate-admin-context-properties/no-match.input.js.map +1 -0
  28. package/dist/transforms/migrate-admin-context-properties/no-match.output.js +30 -0
  29. package/dist/transforms/migrate-admin-context-properties/no-match.output.js.map +1 -0
  30. package/dist/transforms/migrate-displayed-field-value-args/basic.input.js +4 -0
  31. package/dist/transforms/migrate-displayed-field-value-args/basic.input.js.map +1 -0
  32. package/dist/transforms/migrate-displayed-field-value-args/basic.output.js +8 -0
  33. package/dist/transforms/migrate-displayed-field-value-args/basic.output.js.map +1 -0
  34. package/dist/transforms/migrate-displayed-field-value-args/index.d.ts +3 -0
  35. package/dist/transforms/migrate-displayed-field-value-args/index.d.ts.map +1 -0
  36. package/dist/transforms/migrate-displayed-field-value-args/index.js +84 -0
  37. package/dist/transforms/migrate-displayed-field-value-args/index.js.map +1 -0
  38. package/dist/transforms/migrate-displayed-field-value-args/namespace.input.js +4 -0
  39. package/dist/transforms/migrate-displayed-field-value-args/namespace.input.js.map +1 -0
  40. package/dist/transforms/migrate-displayed-field-value-args/namespace.output.js +8 -0
  41. package/dist/transforms/migrate-displayed-field-value-args/namespace.output.js.map +1 -0
  42. package/dist/transforms/migrate-displayed-field-value-args/no-match.input.js +4 -0
  43. package/dist/transforms/migrate-displayed-field-value-args/no-match.input.js.map +1 -0
  44. package/dist/transforms/migrate-displayed-field-value-args/no-match.output.js +4 -0
  45. package/dist/transforms/migrate-displayed-field-value-args/no-match.output.js.map +1 -0
  46. package/dist/transforms/migrate-field-component-types/index.d.ts.map +1 -1
  47. package/dist/transforms/migrate-field-component-types/index.js +4 -0
  48. package/dist/transforms/migrate-field-component-types/index.js.map +1 -1
  49. package/dist/transforms/migrate-sharp-to-transformer/basic.output.js +1 -1
  50. package/dist/transforms/migrate-sharp-to-transformer/basic.output.js.map +1 -1
  51. package/dist/transforms/migrate-sharp-to-transformer/existing-transformers.output.js +1 -1
  52. package/dist/transforms/migrate-sharp-to-transformer/existing-transformers.output.js.map +1 -1
  53. package/dist/transforms/migrate-sharp-to-transformer/index.d.ts.map +1 -1
  54. package/dist/transforms/migrate-sharp-to-transformer/index.js +232 -82
  55. package/dist/transforms/migrate-sharp-to-transformer/index.js.map +1 -1
  56. package/dist/transforms/remove-storage-always-insert-fields/index.d.ts.map +1 -1
  57. package/dist/transforms/remove-storage-always-insert-fields/index.js +4 -0
  58. package/dist/transforms/remove-storage-always-insert-fields/index.js.map +1 -1
  59. package/dist/transforms/rename-diff-value-props/basic.input.js +38 -0
  60. package/dist/transforms/rename-diff-value-props/basic.input.js.map +1 -0
  61. package/dist/transforms/rename-diff-value-props/basic.output.js +38 -0
  62. package/dist/transforms/rename-diff-value-props/basic.output.js.map +1 -0
  63. package/dist/transforms/rename-diff-value-props/index.d.ts +3 -0
  64. package/dist/transforms/rename-diff-value-props/index.d.ts.map +1 -0
  65. package/dist/transforms/rename-diff-value-props/index.js +328 -0
  66. package/dist/transforms/rename-diff-value-props/index.js.map +1 -0
  67. package/dist/transforms/rename-diff-value-props/no-match.input.js +8 -0
  68. package/dist/transforms/rename-diff-value-props/no-match.input.js.map +1 -0
  69. package/dist/transforms/rename-experimental-table-feature/index.d.ts.map +1 -1
  70. package/dist/transforms/rename-experimental-table-feature/index.js +7 -1
  71. package/dist/transforms/rename-experimental-table-feature/index.js.map +1 -1
  72. package/dist/upgrade/defaultTag.d.ts +1 -1
  73. package/dist/upgrade/defaultTag.js +2 -2
  74. package/dist/upgrade/defaultTag.js.map +1 -1
  75. package/dist/upgrade/prompt.js +2 -2
  76. package/dist/upgrade/prompt.js.map +1 -1
  77. package/dist/upgrade/rewritePackageJson.d.ts.map +1 -1
  78. package/dist/upgrade/rewritePackageJson.js +52 -11
  79. package/dist/upgrade/rewritePackageJson.js.map +1 -1
  80. package/dist/utils/project.d.ts +7 -1
  81. package/dist/utils/project.d.ts.map +1 -1
  82. package/dist/utils/project.js +20 -7
  83. package/dist/utils/project.js.map +1 -1
  84. package/package.json +2 -2
@@ -1,13 +1,11 @@
1
1
  ---
2
2
  title: 3.0 to 4.0 Migration Guide
3
3
  label: 3.0 to 4.0
4
- order: 15
4
+ order: 20
5
5
  desc: Upgrade guide for Payload 3.x projects migrating to 4.0.
6
6
  keywords: local api, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react
7
7
  ---
8
8
 
9
- # Payload 3.0 to 4.0 Migration Guide
10
-
11
9
  All breaking changes are listed below. If you encounter changes that are not explicitly listed here, please consider contributing to this documentation by submitting a PR.
12
10
 
13
11
  ## Codemod
@@ -122,9 +120,9 @@ A token is also rejected if its `collection` is not auth-enabled or has `disable
122
120
 
123
121
  See [Token Data](../authentication/token-data) for the full token data shape.
124
122
 
125
- ### `overrideAccess` now defaults to `false` on Local API operations
123
+ ### Local API access is now enforced by default #overrideaccess-now-defaults-to-false-on-local-api-operations
126
124
 
127
- Local API operations whose optional `overrideAccess` value previously defaulted to `true` now default it to `false`, so omitting it respects Access Control instead. Operations that do not expose `overrideAccess`, and operations such as `resetPassword` where it was already required or did not default to `true`, are unaffected by this change.
125
+ `overrideAccess` now defaults to `false` on Local API operations, so omitting it respects Access Control instead. Operations that do not expose `overrideAccess`, and operations such as `resetPassword` where it was already required or did not default to `true`, are unaffected by this change.
128
126
 
129
127
  ```diff
130
128
  const posts = await payload.find({
@@ -156,7 +154,32 @@ Run a typecheck after the codemod. Three patterns it cannot resolve:
156
154
 
157
155
  `payload.jobs.queue`, `run`, `runByID`, `cancel`, and `cancelByID` now also default `overrideAccess` to `false`. The option remains optional, but omitting it runs the corresponding `jobs.access.queue`, `jobs.access.run`, or `jobs.access.cancel` function. The codemod adds `overrideAccess: true` to these calls—including zero-argument `payload.jobs.run()` calls—to preserve Payload 3 behavior for review.
158
156
 
159
- ### Collection and Global Access Control callbacks receive `slug`
157
+ ### On-demand validation adds a `validate` operation
158
+
159
+ The `Operation` type now includes `validate`. The `operation` argument for collection, global, and field `beforeValidate` and `beforeChange` hooks also includes `validate`.
160
+
161
+ On-demand validation runs these hooks without saving the candidate. Update exhaustive operation checks and skip external side effects that should only run during `create` or `update`:
162
+
163
+ ```ts
164
+ import type { CollectionBeforeChangeHook } from 'payload'
165
+
166
+ const syncCustomer: CollectionBeforeChangeHook = async ({
167
+ data,
168
+ operation,
169
+ }) => {
170
+ if (operation === 'validate') {
171
+ return data
172
+ }
173
+
174
+ await syncCustomerWithStripe(data)
175
+
176
+ return data
177
+ }
178
+ ```
179
+
180
+ Sanitized collection and global access types now require a `validate` key. Payload supplies this key during configuration sanitization and uses `update` access by default. Code that constructs a sanitized access object directly must add the key.
181
+
182
+ ### Access Control callbacks now receive document slugs
160
183
 
161
184
  Collection and Global Access Control callback arguments now include the document's `slug`. This additive property is unlikely to affect most projects, but code that manually invokes access callbacks or `executeAccess` must now pass `slug`. Code that validates, enumerates, or compares the exact callback argument shape may also need updating.
162
185
 
@@ -246,7 +269,7 @@ Payload adds built-in command import references to `cli.commands` while sanitizi
246
269
 
247
270
  See the [Payload CLI documentation](../configuration/cli) for the complete API.
248
271
 
249
- ### `migrateCLI` removed
272
+ ### Legacy migration CLI export removed
250
273
 
251
274
  The undocumented `migrateCLI` export has been removed. It accepted an already-sanitized config, a minimist-shaped `parsedArgs` object, and a special `migrationDir` override. This duplicated migration command registration and kept the old argument format alive after the main CLI moved to Commander.
252
275
 
@@ -275,7 +298,7 @@ The spawned CLI discovers the Payload config from `cwd` or `PAYLOAD_CONFIG_PATH`
275
298
 
276
299
  `generateTypes` now returns `{ types, outputFile, written }` instead of returning the generated source directly when using `returnString: true`. `generateImportMap` now returns `{ outputFile, written }`, and `payload.db.generateSchema()` returns `{ outputFile }`. Code that ignores these return values does not need to change.
277
300
 
278
- ### Default query depth reduced to `1`
301
+ ### Default query depth has been reduced
279
302
 
280
303
  Queries that omit `depth` now populate one relationship level instead of two. This reduces database work and response size by default.
281
304
 
@@ -350,7 +373,7 @@ export const Posts: CollectionConfig = {
350
373
  }
351
374
  ```
352
375
 
353
- ### Authorship (`createdBy` / `updatedBy`) is now enabled by default
376
+ ### Authorship is now enabled by default
354
377
 
355
378
  Collections and globals now track authorship by default: Payload injects `createdBy` and `updatedBy` polymorphic relationship fields (related to every auth-enabled collection) and stamps them from `req.user` on each write. Previously there was no authorship tracking; you now opt out with `authorship: false`.
356
379
 
@@ -389,7 +412,7 @@ The `admin.enableListViewSelectAPI` Collection Config property has been removed.
389
412
 
390
413
  If you previously set `admin.enableListViewSelectAPI: true`, remove the property — the behavior is now default. The `migrate-list-view-select-api` transform in `@payloadcms/codemod` handles this automatically.
391
414
 
392
- ### Globals: `admin.components.elements` renamed to `admin.components.edit`
415
+ ### Global component slots now match Collections
393
416
 
394
417
  Global configs now use the same `admin.components` shape as Collections. The Edit View slot container is renamed from `elements` to `edit`, the `Description` slot is hoisted to top-level `admin.components.Description`, and `editMenuItems` is now supported.
395
418
 
@@ -417,7 +440,7 @@ Globals also now support custom views with arbitrary keys under `admin.component
417
440
 
418
441
  This change is auto-migrated by the `globals-components-edit` codemod.
419
442
 
420
- ### `forceSelect` removed in favor of a `select` function on Collections and Globals
443
+ ### Dynamic selection replaces static forced selection
421
444
 
422
445
  The static `forceSelect` config has been removed. Each Collection and Global now accepts a top-level `select` function that receives the current `operation`, `req`, and the caller's `select`, and returns the final `select` to use. Returning `undefined` leaves the caller's `select` unchanged.
423
446
 
@@ -442,9 +465,9 @@ The same change applies to Globals.
442
465
 
443
466
  Run `npx @payloadcms/codemod --transform migrate-force-select` to migrate automatically. The codemod handles object-literal `forceSelect` values, including nested ones (which previously deep-merged) — those are rewritten to call `deepMergeSimple` from `payload/shared`, with the import added automatically. Non-literal values, configs that already define a sibling `select`, and unsupported member kinds are surfaced as notes for manual review.
444
467
 
445
- #### The `admin.hideAPIURL` property on Collections and Globals has been removed
468
+ #### API tab visibility now uses a condition
446
469
 
447
- Use the existing `admin.components.views.edit.api.tab.condition` to hide the API tab instead.
470
+ `admin.hideAPIURL` has been removed from Collections and Globals. Use the existing `admin.components.views.edit.api.tab.condition` to hide the API tab instead.
448
471
 
449
472
  ```diff
450
473
  // collections/Posts.ts
@@ -472,7 +495,7 @@ export const PostsCollection: CollectionConfig = {
472
495
 
473
496
  Run `npx @payloadcms/codemod --transform remove-hide-api-url` to migrate automatically.
474
497
 
475
- ### Aliased type and utility re-exports from `@payloadcms/ui` and `@payloadcms/next`
498
+ ### Aliased UI and Next.js re-exports have been removed
476
499
 
477
500
  Several types and utilities that were re-exported from `@payloadcms/ui` and `@payloadcms/next/utilities` for backwards compatibility have been removed. The canonical exports live in `payload` and `payload/shared`; import directly from there.
478
501
 
@@ -558,7 +581,40 @@ To migrate automatically, there's a codemod for this change available by running
558
581
  npx @payloadcms/codemod --transform migrate-field-component-types
559
582
  ```
560
583
 
561
- ### `DefaultValue` function variant is now typed to allow returning a `Promise` ([#17522](https://github.com/payloadcms/payload/pull/17522))
584
+ ### Diff component props `comparisonValue` and `versionValue` renamed to `valueFrom` and `valueTo`
585
+
586
+ Custom Diff Components (`admin.components.Diff`) receive the two field values being compared as `valueFrom` and `valueTo`. The `comparisonValue` and `versionValue` props have been removed from `FieldDiffClientProps` and `FieldDiffServerProps`, including every field-specific variant such as `TextFieldDiffClientProps`.
587
+
588
+ | Old prop | New prop | Description |
589
+ | ----------------- | ----------- | ------------------------------------------------ |
590
+ | `comparisonValue` | `valueFrom` | Field value from the version being compared from |
591
+ | `versionValue` | `valueTo` | Field value from the version being compared to |
592
+
593
+ ```diff
594
+ import type { TextFieldDiffClientProps } from 'payload'
595
+ import type React from 'react'
596
+
597
+ export const MyTextDiff: React.FC<TextFieldDiffClientProps> = ({
598
+ - comparisonValue,
599
+ - versionValue,
600
+ + valueFrom,
601
+ + valueTo,
602
+ }) => {
603
+ // ...
604
+ }
605
+ ```
606
+
607
+ #### Codemod
608
+
609
+ To migrate automatically, there's a codemod for this change available by running:
610
+
611
+ ```bash
612
+ npx @payloadcms/codemod --transform rename-diff-value-props
613
+ ```
614
+
615
+ The codemod updates functions whose props are typed with a Diff props or component type imported from `payload` (e.g. `TextFieldDiffClientProps`), or with a type in the same file built from one. It renames destructured props and reads such as `props.comparisonValue`. Untyped components and props passed on to other components (e.g. `<OtherDiff comparisonValue={...} />`) aren't changed automatically; the codemod prints a note for each leftover in files that import a Diff type.
616
+
617
+ ### Default value functions can now return promises ([#17522](https://github.com/payloadcms/payload/pull/17522))
562
618
 
563
619
  The exported `DefaultValue` type's function variant previously declared a synchronous-only return type (`SerializableValue`), even though Payload has always awaited the result of a `defaultValue` function at runtime. The type now correctly reflects this and allows `Promise<SerializableValue> | SerializableValue`.
564
620
 
@@ -620,7 +676,7 @@ const interfaceName = global.typescript?.interface
620
676
 
621
677
  Properties filled during sanitization, including `_sanitized`, `admin`, `custom`, `label`, standard access functions, hook arrays, endpoints, and flattened fields, remain required. Use `GlobalConfig` for input and `SanitizedGlobalConfig` after sanitization.
622
678
 
623
- ### User types consolidated into `User` and `AuthenticatedUser`
679
+ ### User types have been consolidated
624
680
 
625
681
  Payload's user types have been consolidated so each one has a clear job. This completes a cleanup already planned in 3.x: `UntypedUser` was deprecated for removal in 4.0, and `TypedUser` was marked to be renamed to `User` in 4.0.
626
682
 
@@ -686,7 +742,7 @@ If the field name is dynamic (configurable at runtime), index through `Record<st
686
742
  const value = (user as Record<string, unknown>)[fieldNameFromConfig]
687
743
  ```
688
744
 
689
- ### `multiTenantPlugin` and `searchPlugin` no longer take a type argument
745
+ ### Plugins now infer generated types automatically
690
746
 
691
747
  The `<ConfigType>` type parameter on `multiTenantPlugin` and `searchPlugin` — and on `MultiTenantPluginConfig` / `SearchPluginConfig` — has been removed. It only existed to override the `user` / `locale` types, which Payload now infers from your generated types automatically: `userHasAccessToAllTenants` receives the generated `User`, and `search`'s `skipSync` callback receives `TypedLocale | undefined`.
692
748
 
@@ -697,9 +753,9 @@ Drop the type argument — your callbacks are typed correctly without it.
697
753
  + multiTenantPlugin({ ... })
698
754
  ```
699
755
 
700
- ### `definePlugin`: `slug` is required and options moved to a named `options` argument
756
+ ### Plugin definitions now require a slug and named options
701
757
 
702
- Two changes to the experimental `definePlugin` helper:
758
+ The experimental `definePlugin` helper now requires a `slug` and passes plugin options through a named `options` argument:
703
759
 
704
760
  - **`slug` is now required.** It registers the plugin in the `plugins` map for cross-plugin discovery. Add one to every `definePlugin` call.
705
761
  - **Options are a named `options` argument.** They were previously spread into the `plugin` callback's args alongside `config` and `plugins`; they're now under a single `options` property. `TOptions` is also no longer constrained to `Record<string, unknown>`, so `interface` and generic option types work.
@@ -713,9 +769,9 @@ export const myPlugin = definePlugin<MyPluginOptions>({
713
769
  })
714
770
  ```
715
771
 
716
- ### `next/navigation` and `next/link` replaced by framework-agnostic `RouterAdapter` hooks
772
+ ### Admin routing is now framework-agnostic
717
773
 
718
- `@payloadcms/ui` no longer depends on `next` directly. Custom admin components (field components, custom views, plugins) that previously imported router hooks or the `Link` component from `next/*` must now import the framework-agnostic equivalents from `@payloadcms/ui`. Behavior is identical when running on Next.js — the `NextRouterAdapter` shipped by `@payloadcms/next` wires `next/navigation` hooks and `next/link` into the same context — but ui code can now also run on non-Next adapters (e.g. TanStack Start) without modification.
774
+ `@payloadcms/ui` no longer depends on `next` directly. Custom admin components (field components, custom views, plugins) that previously imported router hooks or the `Link` component from `next/*` must now import the framework-agnostic equivalents from `@payloadcms/ui`. Behavior is identical when running on Next.js — the `NextRouterAdapter` shipped by `@payloadcms/next` wires `next/navigation` hooks and `next/link` into the same context — but UI code can now also run on non-Next adapters such as TanStack Start without modification.
719
775
 
720
776
  **Hooks** — import from `@payloadcms/ui` instead of `next/navigation`:
721
777
 
@@ -767,7 +823,7 @@ If you were relying on Next-specific extras like `prefetch()` on the router inst
767
823
  />
768
824
  ```
769
825
 
770
- ### `@payloadcms/next/client`, `@payloadcms/next/rsc`, and `@payloadcms/next/templates` removed
826
+ ### Next.js subpath exports have moved to the UI package
771
827
 
772
828
  The `./client`, `./rsc`, and `./templates` subpath exports on `@payloadcms/next` were thin re-exports of components that now live in `@payloadcms/ui`. All admin elements (DocumentHeader, FormHeader, HierarchyTypeField, Logo, Nav) and templates (DefaultTemplate, MinimalTemplate) have been relocated to `@payloadcms/ui` and no longer depend on `next/*` modules. Import them directly from the canonical source.
773
829
 
@@ -805,7 +861,7 @@ If you reference any of these components by string path in your Payload config (
805
861
 
806
862
  Run `npx @payloadcms/codemod --transform migrate-next-subpath-exports` to migrate automatically. It rewrites both `import` declarations and string component paths.
807
863
 
808
- ### `title` and `setDocumentTitle` removed from `useDocumentInfo`
864
+ ### Document title state has moved to its own hook
809
865
 
810
866
  For performance reasons, the document title state has been split out of `DocumentInfoContext` into its own `DocumentTitleContext`. Access it through the `useDocumentTitle` hook, which exposes the same `title` and `setDocumentTitle` API. Components that subscribed to `useDocumentInfo` solely for the title will no longer re-render when unrelated document state changes.
811
867
 
@@ -825,7 +881,7 @@ If you only used `useDocumentInfo` for `title` / `setDocumentTitle`, drop the im
825
881
 
826
882
  Run `npx @payloadcms/codemod --transform migrate-document-title-context` to migrate automatically. The codemod splits mixed destructures, removes the `useDocumentInfo` import when no other properties are still used, and preserves any rename aliases (e.g. `{ title: docTitle }`).
827
883
 
828
- ### `useLocale` returns `null` without an active locale
884
+ ### Locale hooks can now return null
829
885
 
830
886
  The return type of `useLocale` is now `Locale | null`. It returns `null` when localization is disabled or configured without any locales. In Payload 3, the hook was typed as `Locale` and returned an empty object when localization was unavailable.
831
887
 
@@ -855,7 +911,51 @@ When a missing locale is already valid for the receiving API, optional chaining
855
911
  const locale = useLocale()?.code
856
912
  ```
857
913
 
858
- ### RichText adapter `i18n` property removed
914
+ ### `readOnly` removed from the `useField` return type
915
+
916
+ The deprecated `readOnly` property has been removed from `FieldType`, the object returned by `useField`. `useField` already stopped returning it in 3.x, so it was always `undefined` at runtime.
917
+
918
+ Read `readOnly` from your field component's props instead:
919
+
920
+ ```diff
921
+ 'use client'
922
+ import type { TextFieldClientComponent } from 'payload'
923
+ import { useField } from '@payloadcms/ui'
924
+
925
+ -export const MyField: TextFieldClientComponent = ({ path }) => {
926
+ - const { readOnly, value } = useField({ path })
927
+ +export const MyField: TextFieldClientComponent = ({ path, readOnly }) => {
928
+ + const { value } = useField({ path })
929
+ ```
930
+
931
+ ### Deprecated `TableColumnsProvider` props removed
932
+
933
+ The following deprecated props have been removed from `TableColumnsProvider` and its `TableColumnsProviderProps` type:
934
+
935
+ - `docs`
936
+ - `enableRowSelections`
937
+ - `listPreferences`
938
+ - `preferenceKey`
939
+ - `renderRowTypes`
940
+ - `setTable`
941
+ - `sortColumnProps`
942
+ - `tableAppearance`
943
+
944
+ `TableColumnsProvider` has ignored these props since 3.26.0, when column state moved to the URL. Remove them where you render the provider. It now only accepts `children`, `collectionSlug`, `columnState`, and `LinkedCellOverride`.
945
+
946
+ ```diff
947
+ <TableColumnsProvider
948
+ collectionSlug={collectionSlug}
949
+ columnState={columnState}
950
+ - enableRowSelections
951
+ - preferenceKey={preferenceKey}
952
+ - tableAppearance="condensed"
953
+ >
954
+ {children}
955
+ </TableColumnsProvider>
956
+ ```
957
+
958
+ ### Rich text adapters no longer expose translations
859
959
 
860
960
  The `i18n` property on `RichTextAdapter` (and its return type from adapter provider functions) has been removed. It was deprecated in v3 in favour of merging translations directly into `config.i18n.translations` inside the adapter provider.
861
961
 
@@ -912,11 +1012,57 @@ export const MyFeature = createServerFeature({
912
1012
 
913
1013
  If an affected adapter or feature performs asynchronous work, move that work to an asynchronous plugin before config sanitization or to a runtime hook. Projects that only use `buildConfig(...)` and `lexicalEditor(...)` normally are not affected; `buildConfig` remains asynchronous.
914
1014
 
915
- ### `transactionIDPromise` removed from `PayloadRequest`
1015
+ ### Transaction requests now use a single identifier property
916
1016
 
917
1017
  The `transactionIDPromise` property on `PayloadRequest` has been removed. It was deprecated and unused — `transactionID` (which already supports `Promise<number | string>`) is the correct property to use for both resolved values and pending transactions.
918
1018
 
919
- ### Collection `afterOperation` hook: `operation: 'read'` removed
1019
+ ### `headers` and `languageCode` removed from `AdminContext`
1020
+
1021
+ The `headers` and `languageCode` properties have been removed from `AdminContext`, the object returned by `initAdminContext`. Both duplicated values that are already available on `req`:
1022
+
1023
+ ```diff
1024
+ const context = await initAdminContext(args)
1025
+
1026
+ -const userAgent = context.headers.get('user-agent')
1027
+ +const userAgent = context.req.headers.get('user-agent')
1028
+
1029
+ -const languageCode = context.languageCode
1030
+ +const languageCode = context.req.i18n.language
1031
+ ```
1032
+
1033
+ `req.i18n.language` is typed as `string`. Cast it to `AcceptedLanguages` if you need the narrower type.
1034
+
1035
+ `languageCode` has also been removed from `PartialAdminContext`. Use `i18n.language` instead. `getRootLayoutData` from `@payloadcms/ui` no longer accepts `headers` or `languageCode` and reads both from `req`.
1036
+
1037
+ #### Codemod
1038
+
1039
+ To migrate automatically, run:
1040
+
1041
+ ```bash
1042
+ npx @payloadcms/codemod --transform migrate-admin-context-properties
1043
+ ```
1044
+
1045
+ The codemod rewrites property reads and destructures on `initAdminContext()` results and on parameters or variables typed as `AdminContext` or `PartialAdminContext`. It also updates `Pick<AdminContext, ...>` and `AdminContext['...']` types and drops both arguments from `getRootLayoutData()` calls. Element access such as `context['headers']`, and contexts passed through your own wrapper functions, are left unchanged for manual migration.
1046
+
1047
+ ### `DiffMethod` type removed
1048
+
1049
+ The deprecated `DiffMethod` type has been removed from `payload`. It mirrored an enum from `react-diff-viewer-continued`, which Payload no longer depends on. Remove any imports of `DiffMethod`.
1050
+
1051
+ ### `diffMethod` prop removed from Diff components
1052
+
1053
+ Custom Diff components (`admin.components.Diff`) no longer receive a `diffMethod` prop. It has been removed from `FieldDiffClientProps` and every field-specific variant like `TextFieldDiffClientProps`. Payload always passed the same `'diffWordsWithSpace'` value, so the prop carried no information. Each Diff component handles its own diffing.
1054
+
1055
+ Remove `diffMethod` from your props. If you passed it on to a diffing utility, pass `'diffWordsWithSpace'` directly instead:
1056
+
1057
+ ```diff
1058
+ export const MyTextDiff: React.FC<TextFieldDiffClientProps> = ({
1059
+ - diffMethod,
1060
+ valueFrom,
1061
+ valueTo,
1062
+ }) => {
1063
+ ```
1064
+
1065
+ ### Collection after-operation hooks no longer report read operations
920
1066
 
921
1067
  The deprecated `'read'` value for the `operation` argument of collection `afterOperation` hooks has been removed. Payload has not dispatched `afterOperation` with `operation: 'read'` for some time — the `find` and `findByID` operations report their own names (`'find'` and `'findByID'`) instead.
922
1068
 
@@ -939,14 +1085,14 @@ Run `npx @payloadcms/codemod --transform migrate-after-operation-read` to migrat
939
1085
 
940
1086
  The `operation: 'read'` value on the `beforeOperation` hook is unchanged.
941
1087
 
942
- ### `defaults` config export removed from `payload` ([#17103](https://github.com/payloadcms/payload/pull/17103))
1088
+ ### Static configuration defaults have been removed ([#17103](https://github.com/payloadcms/payload/pull/17103))
943
1089
 
944
- The defaults object exported from payload has been removed. It was deprecated because it was a single shared object and mutating it (or any config that used it as a base) leaked changes into every other consumer of the defaults.
1090
+ The `defaults` object exported from `payload` has been removed. It was deprecated because it was a single shared object and mutating it (or any config that used it as a base) leaked changes into every other consumer of the defaults.
945
1091
 
946
1092
  If you depended on reading a specific default value at runtime, read it from the sanitized config returned by buildConfig (or payload.config) instead of from the static defaults object.
947
1093
  Alternatively, you can call `addDefaultsToConfig` to get the unsanitized config but populated with the default properties.
948
1094
 
949
- ### Plugin Import/Export: `toCSV` and `fromCSV` removed
1095
+ ### Import and export field callbacks have moved to hooks
950
1096
 
951
1097
  The `toCSV` and `fromCSV` field options in `custom['plugin-import-export']` have been removed. Use `hooks.beforeExport` and `hooks.beforeImport` instead — they work for both CSV and JSON formats.
952
1098
 
@@ -987,7 +1133,7 @@ The `fromCSV` → `hooks.beforeImport` change is non-breaking for the `data` par
987
1133
 
988
1134
  Run `npx @payloadcms/codemod --transform migrate-import-export-hooks` to migrate automatically. The codemod moves the function values into the correct hook positions and merges with any existing `hooks` object. Review argument names in the migrated functions — `row` references must be renamed to `siblingData` and `doc` references renamed to `data`.
989
1135
 
990
- ### DB Adapter `/types` Subpath Exports Removed
1136
+ ### Database adapter types now use the main package exports
991
1137
 
992
1138
  The `/types` subpath export has been removed from all Drizzle-based database adapter packages. All types are available directly from the main package entry point.
993
1139
 
@@ -1025,19 +1171,28 @@ If you use a `declare module` augmentation to extend `GeneratedDatabaseSchema` (
1025
1171
 
1026
1172
  Run `npx @payloadcms/codemod --transform migrate-db-types-subpath` to migrate automatically. The codemod handles import declarations, re-export declarations, and `declare module` augmentations.
1027
1173
 
1174
+ ### `findMigrationDir` removed from `@payloadcms/drizzle`
1175
+
1176
+ The deprecated `findMigrationDir` re-export has been removed from `@payloadcms/drizzle`. Import it from `payload` instead:
1177
+
1178
+ ```diff
1179
+ - import { findMigrationDir } from '@payloadcms/drizzle'
1180
+ + import { findMigrationDir } from 'payload'
1181
+ ```
1182
+
1028
1183
  ### Minimum supported Node.js version is now 24.15.0 ([#16540](https://github.com/payloadcms/payload/pull/16540))
1029
1184
 
1030
1185
  The minimum supported Node.js version is now **24.15.0** (previously 18.20.2). Upgrade your runtime — along with any CI, Docker images, and deployment targets — to the latest node LTS version or newer before upgrading to 4.0.
1031
1186
 
1032
- ### Minimum supported Next.js version is now 16.2.6 ([#16537](https://github.com/payloadcms/payload/pull/16537))
1187
+ ### Minimum supported Next.js version is now 16.4.0 ([#16537](https://github.com/payloadcms/payload/pull/16537))
1033
1188
 
1034
- Older versions of Next.js are no longer supported. Update Next.js to **16.2.6** or newer.
1189
+ Older versions of Next.js are no longer supported. Update Next.js to **16.4.0** or newer.
1035
1190
 
1036
1191
  ### Minimum supported TypeScript version is now 6.0.3 ([#16692](https://github.com/payloadcms/payload/pull/16692))
1037
1192
 
1038
1193
  Payload's generated types are no longer guaranteed to work on TypeScript versions below **6.0.3** (previously 5.7.3). Upgrade the `typescript` dependency in your project to 6.0.3 or newer.
1039
1194
 
1040
- ### `@payloadcms/richtext-slate` has been removed ([#16359](https://github.com/payloadcms/payload/pull/16359))
1195
+ ### The Slate rich text editor has been removed ([#16359](https://github.com/payloadcms/payload/pull/16359))
1041
1196
 
1042
1197
  Starting in Payload 4.0, `@payloadcms/richtext-lexical` is the only supported rich text editor. The `@payloadcms/richtext-slate` package has been deleted and will no longer be published.
1043
1198
 
@@ -1052,7 +1207,7 @@ The rich text adapter pattern itself is unchanged — `editor: lexicalEditor({})
1052
1207
 
1053
1208
  If you are still using Slate, **do not upgrade to 4.0 yet.** Stay on the latest 3.x release and complete the [Slate → Lexical migration](https://payloadcms.com/docs/rich-text/migration) first.
1054
1209
 
1055
- ### `HTMLConverterFeature` and `lexicalHTML` have been removed ([#16548](https://github.com/payloadcms/payload/pull/16548))
1210
+ ### Legacy Lexical HTML converters have been removed ([#16548](https://github.com/payloadcms/payload/pull/16548))
1056
1211
 
1057
1212
  The deprecated `HTMLConverterFeature`, `lexicalHTML`, and the per-node `converters.html` API have been removed from `@payloadcms/richtext-lexical`. Use `convertLexicalToHTML` and `lexicalHTMLField` from the non-deprecated converter instead.
1058
1213
 
@@ -1076,7 +1231,7 @@ The deprecated `HTMLConverterFeature`, `lexicalHTML`, and the per-node `converte
1076
1231
 
1077
1232
  Custom nodes that define `converters.html` should export their converters instead.
1078
1233
 
1079
- ### Lexical: `isHTMLElement` removed — use lexical's built-in ([#17104](https://github.com/payloadcms/payload/pull/17104))
1234
+ ### Lexical now uses its built-in HTML element check ([#17104](https://github.com/payloadcms/payload/pull/17104))
1080
1235
 
1081
1236
  The deprecated `isHTMLElement` utility has been removed from `@payloadcms/richtext-lexical`. It was a thin wrapper around `x instanceof HTMLElement` that duplicated a utility already exported by `lexical`. Import it directly from `lexical` instead.
1082
1237
 
@@ -1087,9 +1242,9 @@ The deprecated `isHTMLElement` utility has been removed from `@payloadcms/richte
1087
1242
 
1088
1243
  `lexical`'s built-in `isHTMLElement` is the canonical implementation and narrows against the node's owning-document `HTMLElement`, so it is also safe across iframes and other realms.
1089
1244
 
1090
- ### Lexical: vendored `@lexical/markdown` fork removed and lexical upgraded to `0.50.0`
1245
+ ### Lexical now uses the upstream Markdown package
1091
1246
 
1092
- `@payloadcms/richtext-lexical` no longer ships a vendored copy of `@lexical/markdown`. The package is now a direct dependency, every `@lexical/*` package was upgraded from `0.41.0` to `0.50.0`, and `@payloadcms/richtext-lexical/lexical/markdown` is a pure passthrough of `@lexical/markdown`.
1247
+ `@payloadcms/richtext-lexical` no longer ships a vendored copy of `@lexical/markdown`. The package is now a direct dependency, every `@lexical/*` package was upgraded from `0.41.0` to `0.52.0`, and `@payloadcms/richtext-lexical/lexical/markdown` is a pure passthrough of `@lexical/markdown`.
1093
1248
 
1094
1249
  Most projects use markdown through the editor or through `convertMarkdownToLexical` / `convertLexicalToMarkdown` and need no changes. You are only affected if you import from `@payloadcms/richtext-lexical/lexical/markdown` directly.
1095
1250
 
@@ -1113,11 +1268,20 @@ If you keep calling `$convertFromMarkdownString` directly, pass `editorConfig.fe
1113
1268
 
1114
1269
  **Whitespace-only markdown table cells now import as an empty paragraph** instead of a text node of padding spaces. This comes from the lexical upgrade, not Payload's own code. No action is required unless you assert on those padding spaces in stored content.
1115
1270
 
1116
- If you import lexical APIs directly through `@payloadcms/richtext-lexical/lexical/*`, also review the [lexical breaking changes](https://github.com/facebook/lexical/releases) between `0.41.0` and `0.50.0`. None of them require action for typical Payload usage.
1271
+ If you import lexical APIs directly through `@payloadcms/richtext-lexical/lexical/*`, also review the [lexical breaking changes](https://github.com/facebook/lexical/releases) between `0.41.0` and `0.52.0`. None of them require action for typical Payload usage.
1117
1272
 
1118
- ### Lexical: `EXPERIMENTAL_TableFeature` renamed to `TableFeature`
1273
+ If your app imports Lexical types or adds custom features, check these changes from `0.50.0`. They also apply to imports through `@payloadcms/richtext-lexical/lexical` and its subpaths:
1119
1274
 
1120
- The table feature is no longer experimental, so it drops its prefix.
1275
+ - Lexical packages are ESM only. Use ESM imports; CommonJS `require()` needs Node.js 20.19 or later.
1276
+ - `SELECTION_CHANGE_COMMAND` now runs before DOM reconciliation. Defer DOM lookups and floating UI positioning with `$onUpdate(() => editor.read('latest', callback))`, or use an update listener for work that should run after every commit.
1277
+ - Call `node.getLatest().exportJSON()` when serializing a node reference retained across an update. Payload continues to write the full JSON format; compact export is not enabled.
1278
+ - `SerializedEditorState<T>`, `SerializedElementNode<T>`, and `SerializedRootNode<T>` no longer accept a type argument. Remove `<T>` if you only need the base node type. For a document with your own node types, use Payload's `TypedEditorState<T>` instead. Without a type argument, Lexical's `children` are still typed as `SerializedLexicalNode[]`.
1279
+ - `ReturnType<Node['exportJSON']>` can now infer partial JSON, making fields such as `version` and `url` optional. If your code expects the full format, use the node's explicit serialized type, such as `SerializedLinkNode`. A normal `node.exportJSON()` call still returns the full format.
1280
+ - `LexicalNode.getCommonAncestor` was removed. Use `$getCommonAncestor(a, b)?.commonAncestor ?? null`; pass the parents of non-element nodes if you need the old method's behavior.
1281
+
1282
+ ### Lexical table support is no longer experimental
1283
+
1284
+ `EXPERIMENTAL_TableFeature` has been renamed to `TableFeature` now that the table feature is no longer experimental.
1121
1285
 
1122
1286
  ```diff
1123
1287
  - import { EXPERIMENTAL_TableFeature } from '@payloadcms/richtext-lexical'
@@ -1131,7 +1295,45 @@ The table feature is no longer experimental, so it drops its prefix.
1131
1295
 
1132
1296
  Stored content is unaffected.
1133
1297
 
1134
- ### Field `typescriptSchema` renamed to `jsonSchema`
1298
+ ### Lexical: `LexicalEditorProps` renamed to `LexicalEditorArgs`
1299
+
1300
+ The `LexicalEditorProps` type exported from `@payloadcms/richtext-lexical` has been renamed to `LexicalEditorArgs`. It describes the arguments of the `lexicalEditor` function, not component props. The shape is unchanged.
1301
+
1302
+ ```diff
1303
+ - import type { LexicalEditorProps } from '@payloadcms/richtext-lexical'
1304
+ + import type { LexicalEditorArgs } from '@payloadcms/richtext-lexical'
1305
+ ```
1306
+
1307
+ ### Lexical: `UploadDataImproved` replaced by `UploadData` ([#18606](https://github.com/payloadcms/payload/pull/18606))
1308
+
1309
+ The internal `UploadDataImproved` type has been removed, and its definition is now used by the public `UploadData` type. Replace any `UploadDataImproved` imports and type references with `UploadData`:
1310
+
1311
+ ```ts
1312
+ import type { UploadData } from '@payloadcms/richtext-lexical'
1313
+ ```
1314
+
1315
+ ### Lexical: the `version` of nodes is ignored
1316
+
1317
+ Lexical deprecated the `version` property of serialized nodes, and now ignores it when it loads a document. Payload no longer reads it either.
1318
+
1319
+ **Rich text written by the Payload 3.0 betas is no longer upgraded when it's loaded.** Payload used `version: 1` to detect two older shapes:
1320
+
1321
+ - Block nodes whose fields were wrapped in an extra `data` object (`fields.data`)
1322
+ - Relationship and upload nodes that stored the whole related document in `value`, instead of its ID
1323
+
1324
+ If your database may still contain rich text from the 3.0 betas, run `upgradeLexicalData` on the latest 3.x release before you upgrade to 4.0. It re-saves every document, which converts these shapes:
1325
+
1326
+ ```ts
1327
+ import { upgradeLexicalData } from '@payloadcms/richtext-lexical'
1328
+
1329
+ await upgradeLexicalData({ payload })
1330
+ ```
1331
+
1332
+ **`version` is optional in the JSON schema of Lexical nodes.** The CLI and MCP no longer require it, and leave it out of the schema they show agents. They still accept it, so existing documents, which contain `version`, stay valid input.
1333
+
1334
+ **Payload doesn't add or remove `version` when it saves rich text.** Rich text created by Lexical, like in the admin panel, still has `version: 1` on every node, because Lexical's JSON export writes it. Payload's own nodes used to write their own numbers instead (like `2` for blocks), and now write `1` as well. Rich text sent through the API, CLI or MCP is saved as sent, so it may not contain `version`. The generated types still include `version`, marked as deprecated, so don't rely on it being there.
1335
+
1336
+ ### Field schema configuration now reflects JSON Schema
1135
1337
 
1136
1338
  `typescriptSchema` accepted JSON Schema, not TypeScript, so the name lied. Renamed to `jsonSchema`. Same shape, same array of transforms. The underlying schema is now also consumed by the MCP plugin for runtime validation, not just type generation.
1137
1339
 
@@ -1144,9 +1346,9 @@ Stored content is unaffected.
1144
1346
  }
1145
1347
  ```
1146
1348
 
1147
- ### RichText adapter `outputSchema` renamed to `jsonSchema`
1349
+ ### Rich text output schemas have been renamed
1148
1350
 
1149
- Same story on the editor side. The arguments object also gains `typeStringDefinitions: Set<string>` — a sink for raw TS source appended to `payload-types.ts`.
1351
+ The Rich Text adapter's `outputSchema` property has been renamed to `jsonSchema`. The arguments object also gains `typeStringDefinitions: Set<string>` — a sink for raw TS source appended to `payload-types.ts`.
1150
1352
 
1151
1353
  ```diff
1152
1354
  myAdapter: RichTextAdapter = {
@@ -1159,7 +1361,7 @@ Same story on the editor side. The arguments object also gains `typeStringDefini
1159
1361
 
1160
1362
  `@payloadcms/richtext-lexical` is updated. Only third-party adapters need attention.
1161
1363
 
1162
- ### `configToJSONSchema` returns `{ jsonSchema, typeStringDefinitions }`
1364
+ ### Schema generation now returns definitions alongside JSON Schema
1163
1365
 
1164
1366
  `configToJSONSchema` (from `payload`) used to return a `JSONSchema4` directly. It now returns an object with the schema plus the TS-source sink. Destructure to pull the schema out.
1165
1367
 
@@ -1170,7 +1372,7 @@ Same story on the editor side. The arguments object also gains `typeStringDefini
1170
1372
 
1171
1373
  If you run your own type generation, append `[...typeStringDefinitions].join('\n\n')` to the compiled output. Otherwise ignore it.
1172
1374
 
1173
- ### `fieldsToJSONSchema` switched to object arguments
1375
+ ### Field schema generation now uses object arguments
1174
1376
 
1175
1377
  `fieldsToJSONSchema` used to take 6 positional arguments. It now takes one object. The new shape also requires `typeStringDefinitions`, and `forceInlineBlocks` is a top-level sibling instead of nested under `opts`.
1176
1378
 
@@ -1194,9 +1396,9 @@ If you run your own type generation, append `[...typeStringDefinitions].join('\n
1194
1396
  + })
1195
1397
  ```
1196
1398
 
1197
- ### `entityToJSONSchema` gained a required `typeStringDefinitions` argument
1399
+ ### Entity schema generation now requires type definitions
1198
1400
 
1199
- Inserted at position 5 (between `defaultIDType` and `collectionIDFieldTypes`). The old `opts: ConfigToJSONSchemaOptions` is replaced by an optional positional `forceInlineBlocks?: boolean` at the end.
1401
+ `entityToJSONSchema` now requires a `typeStringDefinitions` argument at position 5, between `defaultIDType` and `collectionIDFieldTypes`. The old `opts: ConfigToJSONSchemaOptions` is replaced by an optional positional `forceInlineBlocks?: boolean` at the end.
1200
1402
 
1201
1403
  ```diff
1202
1404
  entityToJSONSchema(
@@ -1212,7 +1414,7 @@ Inserted at position 5 (between `defaultIDType` and `collectionIDFieldTypes`). T
1212
1414
  )
1213
1415
  ```
1214
1416
 
1215
- ### Lexical: `modifyJSONSchema` is gone
1417
+ ### Lexical nodes now provide their own JSON Schemas
1216
1418
 
1217
1419
  `generatedTypes.modifyJSONSchema` on lexical features (and the sanitized `modifyJSONSchemas` array) is removed. Features now contribute per-node JSON Schemas via `createNode({ jsonSchema, node })` instead of mutating a whole-field schema after the fact. The editor composes per-node schemas into the field's union automatically.
1218
1420
 
@@ -1268,7 +1470,7 @@ The same rule applies to lexical block nodes — they now emit as `SerializedBlo
1268
1470
 
1269
1471
  When two **different** blocks resolve to the same auto-derived name (same slug, different fields — e.g. a shared block overridden in one collection), the first keeps the clean name and any later block with a different shape gets a stable content-hash suffix (`Hero_3F2A1B0C`), so neither is silently mistyped. To choose the name yourself — or to resolve a collision with another type in your generated file (a collection, an array's `interfaceName`, etc.) — set an explicit `interfaceName` on the block. See [Block interface name collisions](/docs/typescript/generating-types#block-interface-name-collisions).
1270
1472
 
1271
- ### Blocks field `blockReferences` moved into `blocks`
1473
+ ### Block references now share the blocks array
1272
1474
 
1273
1475
  The `blockReferences` property on blocks fields has been removed. The `blocks` property now accepts both inline block configs and string slugs for blocks defined in the top-level Payload Config `blocks` array.
1274
1476
 
@@ -1299,7 +1501,7 @@ Inline blocks continue to work as before:
1299
1501
  }
1300
1502
  ```
1301
1503
 
1302
- ### Lexical: generated `payload-types.ts` shape changed
1504
+ ### Generated Lexical types are now strict
1303
1505
 
1304
1506
  `richText` fields backed by `lexicalEditor` used to emit a permissive `{ [k: string]: unknown }` per field. They now emit a discriminated union of strict per-node interfaces (`SerializedTextNode`, `SerializedParagraphNode`, `SerializedHeadingNode`, `SerializedBlockNode`, etc.) under a hash-named alias like `LexicalNodes_AB12CD34`.
1305
1507
 
@@ -1330,7 +1532,7 @@ Element nodes are now generic over their `children`, and you pass the node union
1330
1532
 
1331
1533
  `SerializedTextNode` is a leaf, so it stays bare. For the built-in nodes plus a few of your own, `WithDefaultNodes` threads the union for you - pass your custom node(s) as the generic (`type MyNodes = WithDefaultNodes<SerializedBlockNode<MyBlockData>>`), and they're usable anywhere a node can appear, not just at the top level. `DefaultTypedEditorState` with only built-in nodes needs no change.
1332
1534
 
1333
- ### Generated JSON Schema uses `$defs` instead of `definitions`
1535
+ ### Generated JSON Schema now uses modern definitions
1334
1536
 
1335
1537
  Payload builds a JSON Schema from your config to generate `payload-types.ts`. That schema now stores its reusable types under the modern `$defs` key instead of the older `definitions` key, and every internal reference points at `#/$defs/...` instead of `#/definitions/...`. This follows the current JSON Schema spec.
1336
1538
 
@@ -1388,7 +1590,7 @@ jobs: {
1388
1590
 
1389
1591
  Job documents can contain inputs, outputs, logs, and execution state, so avoid enabling write access unless your application specifically requires it.
1390
1592
 
1391
- ### Jobs: `jobs.depth` and `jobs.runHooks` have been removed ([#16414](https://github.com/payloadcms/payload/pull/16414))
1593
+ ### Job queries and hooks now have fixed behavior ([#16414](https://github.com/payloadcms/payload/pull/16414))
1392
1594
 
1393
1595
  The deprecated `jobs.depth` and `jobs.runHooks` config options have been removed. Job operations now always use direct database calls with depth `0`.
1394
1596
 
@@ -1396,7 +1598,7 @@ If you relied on either option, remove it from your `jobs` config. If you depend
1396
1598
 
1397
1599
  The `jobsCollectionOverrides` warning that detected extra hooks and prompted users to set `runHooks` has also been removed.
1398
1600
 
1399
- ### Jobs: `BaseJob` has been removed
1601
+ ### The legacy base job type has been removed
1400
1602
 
1401
1603
  The deprecated `BaseJob` type is no longer exported. Use `Job` everywhere you previously used `BaseJob`.
1402
1604
 
@@ -1454,9 +1656,9 @@ The deprecated `RunningJob` and `RunningJobSimple` types have also been removed.
1454
1656
 
1455
1657
  JSON workflow steps without a `task` must now provide an `inlineTask` handler. This removes a type escape that previously allowed an incomplete step even though it could not run.
1456
1658
 
1457
- ### Jobs: custom database adapters must implement `updateJobs`
1659
+ ### Custom database adapters must support job claiming
1458
1660
 
1459
- Payload now uses a unique `processingToken` when claiming jobs so multiple workers cannot run the same job. This adds an internal, nullable `processingToken` field to the `payload-jobs` collection. Existing SQL databases need a migration that adds this column. MongoDB does not require a migration.
1661
+ Custom database adapters must now implement `updateJobs` so Payload can use a unique `processingToken` when claiming jobs and prevent multiple workers from running the same job. This adds an internal, nullable `processingToken` field to the `payload-jobs` collection. Existing SQL databases need a migration that adds this column. MongoDB does not require a migration.
1460
1662
 
1461
1663
  The default `updateJobs` implementation has been removed because it could not safely claim jobs across multiple workers. If you maintain a custom database adapter, you must now provide `updateJobs`. It must only claim jobs that still match the provided `where` condition and, when a `processingToken` is provided, return only the jobs claimed with that token. The official Payload database adapters already implement this behavior.
1462
1664
 
@@ -1468,7 +1670,7 @@ Worker updates use a 30-second safety buffer by default, so an update only start
1468
1670
 
1469
1671
  Existing SQL databases need a migration that replaces `processing` with the nullable, indexed `processingUntil` column. MongoDB does not require a migration.
1470
1672
 
1471
- ### Jobs: the `state` field on task return values has been removed ([#16416](https://github.com/payloadcms/payload/pull/16416))
1673
+ ### Job task results no longer include state ([#16416](https://github.com/payloadcms/payload/pull/16416))
1472
1674
 
1473
1675
  Job task handlers and `inlineTask` callbacks can no longer use the `state` field to signal success or failure. Both shapes were deprecated in v3 and are now removed from tasks and inline tasks.
1474
1676
 
@@ -1493,7 +1695,7 @@ const myTask: TaskHandler<'MyTask'> = async ({ input }) => {
1493
1695
  }
1494
1696
  ```
1495
1697
 
1496
- ### Croner `sloppyRanges` compatibility setting removed ([#16546](https://github.com/payloadcms/payload/pull/16546))
1698
+ ### Cron expressions now require explicit ranges ([#16546](https://github.com/payloadcms/payload/pull/16546))
1497
1699
 
1498
1700
  The `sloppyRanges: true` setting that kept Croner v9 stepping syntax working after the v10 upgrade has been removed. Croner now enforces stricter cron parsing rules, so stepping expressions must use either `*` or an explicit range before the `/`.
1499
1701
 
@@ -1525,7 +1727,7 @@ Migrate any affected cron expressions:
1525
1727
  + cron: '30 * * * *'
1526
1728
  ```
1527
1729
 
1528
- ### `min` and `max` removed from `relationship` and `upload` fields ([#16547](https://github.com/payloadcms/payload/pull/16547))
1730
+ ### Relationship and upload limits now use row-based options ([#16547](https://github.com/payloadcms/payload/pull/16547))
1529
1731
 
1530
1732
  The deprecated `min` and `max` properties have been removed from `relationship` and `upload` fields with `hasMany: true`. Use `minRows` and `maxRows` instead.
1531
1733
 
@@ -1544,7 +1746,7 @@ The deprecated `min` and `max` properties have been removed from `relationship`
1544
1746
 
1545
1747
  The same change applies to `upload` fields with `hasMany: true`.
1546
1748
 
1547
- ### `allowLocalizedWithinLocalized` compatibility flag removed ([#16693](https://github.com/payloadcms/payload/pull/16693))
1749
+ ### Nested localized fields no longer need a compatibility flag ([#16693](https://github.com/payloadcms/payload/pull/16693))
1548
1750
 
1549
1751
  `config.compatibility.allowLocalizedWithinLocalized` and the `PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY` environment variable have been removed. Sanitize no longer strips `localized: true` from fields nested under a localized parent — `fieldShouldBeLocalized` decides this at runtime instead.
1550
1752
 
@@ -1558,7 +1760,7 @@ The same change applies to `upload` fields with `hasMany: true`.
1558
1760
  - Remove the `compatibility` block from your config. If your MongoDB data still has the redundant nested-localized shape, flatten the config and run a data migration.
1559
1761
  - Replace any direct `field.localized` reads in custom code with `fieldShouldBeLocalized({ field, parentIsLocalized })` from `payload/shared`.
1560
1762
 
1561
- ### Admin UI Publish button now defaults to the active locale, and `localization.defaultLocalePublishOption` has been removed
1763
+ ### Publishing now defaults to the active locale
1562
1764
 
1563
1765
  The Admin UI Publish button now always defaults to publishing only the active locale when a collection or global has localized fields and you have publish permission. "Publish all locales" is available as a secondary option in the Publish button's dropdown. This previously required opting in via `localization.defaultLocalePublishOption: 'active'`; the config option has been removed since active-locale-only is now the permanent default.
1564
1766
 
@@ -1580,7 +1782,7 @@ export default buildConfig({
1580
1782
 
1581
1783
  Run `npx @payloadcms/codemod --transform remove-default-locale-publish-option` to remove the property automatically. The codemod surfaces a note for any config that had it set to `'all'`, since that combination previously changed the default and now has no config equivalent.
1582
1784
 
1583
- ### Storage adapters must be declared under `storage`, not `plugins` ([#16596](https://github.com/payloadcms/payload/pull/16596))
1785
+ ### Storage adapters have moved to top-level configuration ([#16596](https://github.com/payloadcms/payload/pull/16596))
1584
1786
 
1585
1787
  Storage adapter packages (`@payloadcms/storage-s3`, `-azure`, `-gcs`, `-r2`, `-vercel-blob`) must now be placed under the top-level `storage` key in your Payload config instead of inside `plugins`.
1586
1788
 
@@ -1668,6 +1870,8 @@ If you typed `clientUploads.access`, replace `ClientUploadsAccess` with `UploadI
1668
1870
 
1669
1871
  #### If you maintain a custom storage adapter
1670
1872
 
1873
+ File versioning adds a required `GeneratedAdapter.copyFile` method. Copy `from` to `to` (complete storage keys) without changing the source, reject an existing destination, and return only once the destination is readable. `moveFile` stays optional. See [Custom Storage Adapters](../upload/storage-adapters) for the full contract.
1874
+
1671
1875
  Replace `clientUploadContext` with `uploadReference` in generated file metadata and static handler parameters. `handleUpload` no longer receives it because Payload skips uploading files that are already stored.
1672
1876
 
1673
1877
  Set `uploadInstructions.enabled`. When false, Payload adds `adminHandler` to the import map without rendering it. When true, Payload also renders it and exposes the instructions and supporting endpoint.
@@ -1750,7 +1954,7 @@ uploadInstructions: {
1750
1954
 
1751
1955
  Both are optional. Custom handler props are now passed as `props`, and the supporting route is available as `endpointPath` instead of `serverHandlerPath`. The old Admin component `enabled` prop was removed.
1752
1956
 
1753
- ### `@payloadcms/storage-uploadthing` has been removed
1957
+ ### The Uploadthing storage adapter has been removed
1754
1958
 
1755
1959
  The Uploadthing storage adapter, deprecated in a previous release, has been deleted and will no longer be published. Migrate to another storage adapter such as `@payloadcms/storage-s3` before upgrading to 4.0.
1756
1960
 
@@ -1805,15 +2009,17 @@ azureStorage({
1805
2009
 
1806
2010
  Run `npx @payloadcms/codemod --transform migrate-azure-chunk-large-files` to remove the flag automatically (it collapses `clientUploads: { chunkLargeFiles: true }` to `clientUploads: true` and keeps any other `clientUploads` options).
1807
2011
 
1808
- ### Sharp moved out of core into `@payloadcms/transformer-sharp`
2012
+ ### Sharp is now an optional file transformer #sharp-moved-out-of-core-into-payloadcmstransformer-sharp
1809
2013
 
1810
2014
  Image resizing is no longer built into `payload` itself. It now ships as an official [file transformer](../upload/transformers), `@payloadcms/transformer-sharp`, that you register under `upload.transformers`.
1811
2015
 
1812
- This affects three things:
2016
+ This affects four things:
1813
2017
 
1814
2018
  1. The top-level `sharp` config property has been removed. Passing a Sharp instance now happens through `sharpTransformer({ sharp })` instead.
1815
2019
  2. Per-collection Sharp options — `constructorOptions`, `formatOptions`, `resizeOptions`, `trimOptions`, and `withMetadata` — have been removed from `CollectionConfig['upload']`. They're now authored through `sharpTransformer({ collections })`.
2020
+ - `imageSizes` has also been removed from `CollectionConfig['upload']`. Image sizes are now declared as `variants` on `sharpTransformer({ collections: { <slug>: { variants } } })`. The entries themselves (`name`, `width`, `height`, `generateImageName`, and so on) are unchanged. A collection that still sets `upload.imageSizes` fails the build.
1816
2021
  3. `sharp` is no longer a dependency of `payload` at all (previously a `devDependency`, not a runtime `dependency`). Install `@payloadcms/transformer-sharp` explicitly.
2022
+ 4. The Sharp-specific types `SharpDependency`, `ImageUploadFormatOptions`, `ImageUploadTrimOptions`, and `SharpImageSizeOptions` are no longer exported from `payload`. Import `SharpDependency` from `@payloadcms/transformer-sharp`. For the option types, use `SharpCollectionConfig['formatOptions']`, `SharpCollectionConfig['trimOptions']`, and `NonNullable<SharpCollectionConfig['variants']>[number]` from the same package.
1817
2023
 
1818
2024
  ```sh
1819
2025
  pnpm add @payloadcms/transformer-sharp
@@ -1841,7 +2047,7 @@ pnpm add @payloadcms/transformer-sharp
1841
2047
  + collections: {
1842
2048
  + media: {
1843
2049
  + resizeOptions: { width: 2048, height: 2048 },
1844
- + imageSizes: [{ name: 'thumbnail', width: 400, height: 300 }],
2050
+ + variants: [{ name: 'thumbnail', width: 400, height: 300 }],
1845
2051
  + },
1846
2052
  + },
1847
2053
  + }),
@@ -1850,9 +2056,9 @@ pnpm add @payloadcms/transformer-sharp
1850
2056
  })
1851
2057
  ```
1852
2058
 
1853
- Run `npx @payloadcms/codemod --transform migrate-sharp-to-transformer` to migrate automatically. The codemod moves a top-level `sharp` dependency and inline collection Sharp options into `sharpTransformer({ collections })`, appends to an existing `upload.transformers` array if you already have one, and leaves anything it can't safely rewrite in place with a note — for example, a `collections` array built by a function call, or a collection defined outside the `buildConfig` call. Review those manually.
2059
+ Run `npx @payloadcms/codemod --transform migrate-sharp-to-transformer` to migrate automatically. The codemod moves a top-level `sharp` dependency and inline collection Sharp options — including `crop` and `focalPoint` — into `sharpTransformer({ collections })` (writing `imageSizes` as `variants`), appends to an existing `upload.transformers` array if you already have one, and leaves anything it can't safely rewrite in place with a note — for example, a `collections` array built by a function call, or a collection defined outside the `buildConfig` call. Review those manually. For a config that already registers `sharpTransformer`, it only renames `imageSizes` to `variants` inside the existing `sharpTransformer` call and flags a leftover top-level `sharp` property; it doesn't move any remaining collection options.
1854
2060
 
1855
- `imageSizes`, `crop`, and `focalPoint` are still readable at `collection.upload.imageSizes`/`.crop`/`.focalPoint` — `sharpTransformer` writes a projection of what you configure back onto the sanitized collection at startup, so the Admin Panel, generated types, and the stored `sizes` shape on your documents are unchanged. This write-back is why `crop`/`focalPoint`/`imageSizes` are not in the list of removed options above — only the Sharp-specific processing options moved.
2061
+ Your `variants`, `crop`, and `focalPoint` are readable at `collection.upload.variants`/`.crop`/`.focalPoint` on the **sanitized** config (for example `req.payload.collections.media.config.upload.variants`). Code that read the sanitized `upload.imageSizes` in 3.x must read `upload.variants` instead. `sharpTransformer` writes a projection of what you configure back onto the sanitized collection at startup, so the Admin Panel and the rest of core see the same configuration. For a collection configured on `sharpTransformer`, a `crop` or `focalPoint` set on the transformer replaces the collection's own value; one left unset there keeps the collection's. Neither `imageSizes` nor `variants` can be set on the collection at all.
1856
2062
 
1857
2063
  If you inject a custom Sharp build or version, pass it the same way:
1858
2064
 
@@ -1861,9 +2067,69 @@ If you inject a custom Sharp build or version, pass it the same way:
1861
2067
  + sharpTransformer({ sharp: myCustomSharp })
1862
2068
  ```
1863
2069
 
1864
- `sharpTransformer` can also resize images at request time (`/api/media/file/photo.png?width=400`). This is new in Payload 4 and **disabled by default**, so a migrated config keeps serving only the original file and your pre-generated `imageSizes`, as in Payload 3.x. The codemod doesn't enable it. To opt in, pass `dynamic: true` or `dynamic: { collections: ['media'] }` — see [Dynamic (request-time) resizing](../upload/transformers#dynamic-request-time-resizing), and [Securing dynamic resizing](../upload/transformers#securing-dynamic-resizing) before enabling it on a publicly readable collection.
2070
+ `sharpTransformer` can also resize images at request time (`/api/media/file/photo.png?width=400`). This is new in Payload 4 and **disabled by default**, so a migrated config keeps serving only the original file and your pre-generated `variants`. The codemod doesn't enable it. To opt in, pass `dynamic: true` or `dynamic: { collections: ['media'] }` — see [Dynamic (request-time) resizing](../upload/transformers#dynamic-request-time-resizing), and [Securing dynamic resizing](../upload/transformers#securing-dynamic-resizing) before enabling it on a publicly readable collection.
1865
2071
 
1866
- ### Storage adapters always insert the `prefix` field now — `alwaysInsertFields` removed
2072
+ #### Upload image data now uses variants
2073
+
2074
+ The group field that holds each generated image size on an upload document is now `variants` instead of `sizes`. Every place that reads or queries it changes:
2075
+
2076
+ ```diff
2077
+ - doc.sizes?.thumbnail?.url
2078
+ + doc.variants?.thumbnail?.url
2079
+
2080
+ - where: { 'sizes.thumbnail.filename': { equals: 'photo-400x300.png' } }
2081
+ + where: { 'variants.thumbnail.filename': { equals: 'photo-400x300.png' } }
2082
+
2083
+ - select: { sizes: { thumbnail: true } }
2084
+ + select: { variants: { thumbnail: true } }
2085
+ ```
2086
+
2087
+ GraphQL queries select `variants { thumbnail { url } }` instead of `sizes { thumbnail { url } }`, and regenerated types expose `variants?: { ... }` (and `variants?: T | { ... }` on the `*Select` types) in place of `sizes`. Update any frontend code that reads `media.sizes`, including values you pass to helpers such as `getBestFitFromSizes({ sizes: doc.variants })`.
2088
+
2089
+ Existing documents must be migrated. Create the predefined migration for your database adapter and run it:
2090
+
2091
+ <Banner type="warning">
2092
+ Payload 4 uses `variants` as the top-level field for generated upload
2093
+ variants. If an upload collection already defines a custom field named
2094
+ `variants`, rename that field before you run the predefined migration. Choose
2095
+ a new name, update your config, and write a custom migration that moves the
2096
+ existing custom data from `variants` to the new field. Then run the predefined
2097
+ `sizes-to-variants` migration. If legacy `sizes` data or columns and custom
2098
+ `variants` data or columns are both present, the predefined migration stops
2099
+ before it changes data or indexes. It cannot choose a new name or merge this
2100
+ custom data for you.
2101
+ </Banner>
2102
+
2103
+ ```sh
2104
+ # MongoDB
2105
+ payload migrate:create --file @payloadcms/db-mongodb/sizes-to-variants
2106
+ # Postgres
2107
+ payload migrate:create --file @payloadcms/db-postgres/sizes-to-variants
2108
+ # Vercel Postgres
2109
+ payload migrate:create --file @payloadcms/db-vercel-postgres/sizes-to-variants
2110
+ # SQLite
2111
+ payload migrate:create --file @payloadcms/db-sqlite/sizes-to-variants
2112
+ # Cloudflare D1
2113
+ payload migrate:create --file @payloadcms/db-d1-sqlite/sizes-to-variants
2114
+
2115
+ payload migrate
2116
+ ```
2117
+
2118
+ **Postgres and SQLite (including Vercel Postgres and D1):** the columns are renamed in place from `sizes_<name>_<property>` to `variants_<name>_<property>`, on each upload collection's table and its versions table, along with their indexes. Use the predefined migration above **instead of** a plain `payload migrate:create` for this change. A generated migration would drop the `sizes_*` columns and add new `variants_*` columns, losing the stored image size data. The predefined migration also writes the schema snapshot, so the next generated migration starts from the renamed columns.
2119
+
2120
+ On MongoDB the migration renames `sizes` (and `version.sizes` on versions) with `$rename` and re-creates the indexes on those fields under the new path. Run the migration before the upgraded app writes to upload documents.
2121
+
2122
+ #### The file endpoint changes once any transformer is registered
2123
+
2124
+ Registering any transformer under `upload.transformers` — including `sharpTransformer()` with dynamic resizing off — routes every file request through the transformer-aware handler:
2125
+
2126
+ - A file must belong to an upload document to be served. A file that exists in storage without a matching document now returns `404`.
2127
+ - For files whose MIME type a transformer declares (every image, with `sharpTransformer()`), `access.read` can run twice per request: once with `req.fileTransform` set to `true` to decide a possible transform, and once without it for an ordinary read. Keep `access.read` cheap, and use `req.fileTransform` to restrict transformed reads — see [Access control](../upload/transformers#access-control).
2128
+ - Responses produced by a transformer always carry Payload's CORS headers, which `modifyResponseHeaders` can't override.
2129
+
2130
+ To link to a file from your own code, including with transformer query parameters, use the new `generatePayloadFileURL` helper (exported from `payload` and `payload/shared`). It always builds the access-controlled Payload file endpoint, so links never bypass `access.read` by pointing straight at storage.
2131
+
2132
+ ### Storage prefixes are now always included
1867
2133
 
1868
2134
  The `alwaysInsertFields` option (on `cloudStoragePlugin` and every `storage-*` adapter: `-s3`, `-azure`, `-gcs`, `-r2`, `-vercel-blob`) has been removed. The behavior it used to opt into — always inserting the hidden `prefix` field into the collection schema — is now unconditional, so the schema stays consistent whether or not a collection configures a `prefix` and whether or not the plugin is `enabled`.
1869
2135
 
@@ -1878,7 +2144,13 @@ The `alwaysInsertFields` option (on `cloudStoragePlugin` and every `storage-*` a
1878
2144
  - Remove `alwaysInsertFields` from your storage adapter or `cloudStoragePlugin` config — it has no effect and no longer exists on the option types.
1879
2145
  - For Postgres, SQLite, or D1 projects, run `payload migrate:create` to generate the migration that adds the `prefix` column, then run `payload migrate` to apply it.
1880
2146
 
1881
- ### `FileSizeImproved` removed — use `FileSize` ([#16593](https://github.com/payloadcms/payload/pull/16593))
2147
+ ### Upload collections retain originals and file history
2148
+
2149
+ Upload collections now store `original` metadata and a `prefix` and `_objectKey` for each stored file (the main file, `original`, and each variant), so saved versions can find their files. New uploads keep their source under an `-original` filename. Update any code or infrastructure that relies on the old filename, URL, or object key.
2150
+
2151
+ **Postgres and SQLite:** after updating, run `payload migrate:create`, check that it adds these columns to each upload collection table and its versions table, then run `payload migrate`. Existing rows keep `null` values until their next write.
2152
+
2153
+ ### File size types have been consolidated ([#16593](https://github.com/payloadcms/payload/pull/16593))
1882
2154
 
1883
2155
  `FileSizeImproved` has been merged into `FileSize`. The `url`, `width`, `height`, `filesize`, `mimeType`, and `filename` properties now accept `null` directly on `FileSize`, matching what the database stores for sizes that were not generated.
1884
2156
 
@@ -1887,9 +2159,9 @@ The `alwaysInsertFields` option (on `cloudStoragePlugin` and every `storage-*` a
1887
2159
  + import type { FileSize } from 'payload'
1888
2160
  ```
1889
2161
 
1890
- ### `ImageSize.admin` — `disableListColumn`, `disableListFilter`, and `disableGroupBy` removed ([#16593](https://github.com/payloadcms/payload/pull/16593))
2162
+ ### Image size admin controls have been consolidated ([#16593](https://github.com/payloadcms/payload/pull/16593))
1891
2163
 
1892
- The three individual boolean props have been replaced with a single `disabled` object, consistent with the shape used by all other fields.
2164
+ The `disableListColumn`, `disableListFilter`, and `disableGroupBy` properties under `ImageSize.admin` have been replaced with a single `disabled` object, consistent with the shape used by all other fields.
1893
2165
 
1894
2166
  ```diff
1895
2167
  {
@@ -1906,7 +2178,7 @@ The three individual boolean props have been replaced with a single `disabled` o
1906
2178
 
1907
2179
  Run `npx @payloadcms/codemod --transform migrate-disabled-fields` to migrate automatically.
1908
2180
 
1909
- ### `db-mongodb`: `connectOptions.useFacet` removed ([#16612](https://github.com/payloadcms/payload/pull/16612))
2181
+ ### MongoDB queries no longer use facet aggregation ([#16612](https://github.com/payloadcms/payload/pull/16612))
1910
2182
 
1911
2183
  The `useFacet` option in `mongooseAdapter` config has been removed. Payload no longer uses `$facet` aggregation, so the option has no effect.
1912
2184
 
@@ -1918,7 +2190,7 @@ The `useFacet` option in `mongooseAdapter` config has been removed. Payload no l
1918
2190
  })
1919
2191
  ```
1920
2192
 
1921
- ### `plugin-search`: `apiBasePath` config option removed ([#16597](https://github.com/payloadcms/payload/pull/16597))
2193
+ ### Search plugin API paths are now derived automatically ([#16597](https://github.com/payloadcms/payload/pull/16597))
1922
2194
 
1923
2195
  The deprecated `apiBasePath` option on the search plugin has been removed. The plugin now derives the correct path from the root config automatically.
1924
2196
 
@@ -1930,7 +2202,7 @@ The sha1 HMAC fallback for API key authentication has been removed. API keys are
1930
2202
 
1931
2203
  **How to migrate:** Open the user document for each affected API key holder and save it — this regenerates the key under sha256. Alternatively, generate a new API key from the admin panel.
1932
2204
 
1933
- ### `baseAPIKeyFields` was replaced by `createAPIKeyFields`
2205
+ ### API key fields now use a factory
1934
2206
 
1935
2207
  The `baseAPIKeyFields` export from `payload` has been removed. Use `createAPIKeyFields()` instead. It returns the same default auth API key storage fields, and also accepts overrides for projects or plugins that need to customize the generated field configs.
1936
2208
 
@@ -1959,9 +2231,9 @@ createAPIKeyFields({
1959
2231
  })
1960
2232
  ```
1961
2233
 
1962
- ### `@payloadcms/plugin-mcp` refactored: config API, built-in tool inputs, and user API keys
2234
+ ### MCP plugin configuration and authentication have changed
1963
2235
 
1964
- The MCP plugin was refactored. The public config API, the built-in tool inputs, and API-key authentication changed. Update your plugin config and recreate MCP credentials as user API keys.
2236
+ `@payloadcms/plugin-mcp` has a new public config API, built-in tool inputs, and API-key authentication flow. Update your plugin config and recreate MCP credentials as user API keys.
1965
2237
 
1966
2238
  **Collections and globals are now opt-out.** Drop the `enabled` flag. Every collection and global is exposed by default through the generic built-in tools. For collections, this includes tools like `getCollectionSchema`, `findDocuments`, `countDocuments`, `createDocuments`, `updateDocument`, `deleteDocuments`, `duplicateDocument`, and `findDistinct`, plus version tools when versions are enabled. For globals, this includes `getGlobalSchema`, `findGlobal`, and `updateGlobal`, plus version tools when versions are enabled. Turn individual operations off via simple config keys like `tools: { create: false }`.
1967
2239
 
@@ -2132,7 +2404,9 @@ By default, MCP items require an authenticated user. Built-in collection and glo
2132
2404
 
2133
2405
  **Dependencies.** The plugin now builds on `@modelcontextprotocol/server` instead of `@modelcontextprotocol/sdk` + `mcp-handler`. It no longer depends on Zod directly; its built-in tools use the Zod Mini export from `payload`. If your custom tools use regular Zod, install Zod v4 in your project and import it directly.
2134
2406
 
2135
- ### `@payloadcms/ui/scss` export removed and legacy `--theme-*` tokens retired
2407
+ **HTTP requests have a size limit.** `POST /api/mcp` now answers request bodies over 4 MiB with `413`. Base64 file uploads count toward this limit. Raise it with `mcp.maxRequestBodySize`.
2408
+
2409
+ ### Admin UI styles now use CSS and semantic tokens
2136
2410
 
2137
2411
  The Admin UI styles were migrated from SCSS to plain CSS. The `@payloadcms/ui/scss` package export and its SCSS source files have been removed, and the legacy `--theme-*` CSS variables were retired in favor of semantic `--color-*` tokens and the raw `--ramp-*` color palette.
2138
2412
 
@@ -2208,7 +2482,7 @@ If you previously used `rest: false`, remove the property; omitting `rest` disab
2208
2482
  - `hasAPIKey` (3.x virtual field) is removed. Use `apiKeyLast4` and `docPermissions.fields.apiKey`.
2209
2483
  - Custom `access.read` on the generated `apiKey` field is inert — reads are hardcoded to deny.
2210
2484
 
2211
- ### Upload `abortOnLimit` now defaults to `true`
2485
+ ### Uploads now reject oversized files by default
2212
2486
 
2213
2487
  The Payload-wide [`upload.abortOnLimit`](../upload/overview#payload-wide-upload-options) option now defaults to `true`. A file that is bigger than the configured `fileSize` limit now returns an HTTP 413 error by default. Previously, the file was truncated to the limit without any error, and the upload continued.
2214
2488
 
@@ -2224,3 +2498,19 @@ export default buildConfig({
2224
2498
  },
2225
2499
  })
2226
2500
  ```
2501
+
2502
+ ### Nodemailer adapter now uses nodemailer 10 ([#18737](https://github.com/payloadcms/payload/pull/18737))
2503
+
2504
+ `@payloadcms/email-nodemailer` now depends on nodemailer 10, which ships its own types. If you pass your own `transport` created with nodemailer 9, it no longer type-checks against the adapter. Upgrade `nodemailer` and remove `@types/nodemailer`:
2505
+
2506
+ ```diff
2507
+ "dependencies": {
2508
+ - "nodemailer": "^9.1.1"
2509
+ + "nodemailer": "^10.1.1"
2510
+ },
2511
+ "devDependencies": {
2512
+ - "@types/nodemailer": "^8.0.0"
2513
+ }
2514
+ ```
2515
+
2516
+ If you use `transportOptions`, or pass no config, no changes are needed.