payloadcms / payloadcms/payload
plugin-nested-docs: duplicate breadcrumb IDs across locales cause unique constraint violation on publish
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 44.8k
- Forks
- 4.2k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 53
Description
Describe the Bug
When using @payloadcms/plugin-nested-docs with localized breadcrumbs and PostgreSQL, publishing a document that has breadcrumbs in multiple locales can fail with:
ValidationError: The following field is invalid: id
"Value must be unique"
The underlying PostgreSQL error is:
duplicate key value violates unique constraint "pages_breadcrumbs_pkey"
Key (id)=(69c3b6df18d83ffbcc032db3) already exists.
Root Cause
In formatBreadcrumb.js, the existing breadcrumb is spread into the new one:
return {
...breadcrumb || {}, // carries over the existing `id`
doc: lastDoc.id,
label,
url
};
And in populateBreadcrumbs.js, the breadcrumb is read from the current document at index i:
breadcrumb: currentDocument[breadcrumbsFieldName]?.[i],
The beforeChange hook runs once per save. When Payload publishes, it writes all locales. Since breadcrumbs are localized, each locale gets its own row in a _breadcrumbs table with id as the primary key. But formatBreadcrumb preserves the id from the spread, so if one locale's breadcrumb was initially seeded from another locale's data, both end up with the same id. When Payload inserts both rows in the same transaction, the second insert violates the primary key constraint.
Expected Behavior
Each breadcrumb row should receive a unique id, regardless of locale. formatBreadcrumb should generate a fresh id instead of inheriting it from the spread.
Workaround
Add a beforeChange hook on the collection that forces unique IDs on breadcrumbs:
({ data }) => {
if (Array.isArray(data?.breadcrumbs)) {
for (const crumb of data.breadcrumbs) {
if (crumb && typeof crumb === 'object') {
crumb.id = [...Array(24)].map(() => Math.floor(Math.random() * 16).toString(16)).join('')
}
}
}
return data
},
Link to the code that reproduces this issue
https://github.com/fkkehlet/payload-nested-docs-breadcrumb-bug
Reproduction Steps
I observed that two locales ended up with the same breadcrumb id in production, causing a unique constraint violation on publish.
I was unable to reliably reproduce the exact admin UI sequence that causes this, but the underlying issue looks to be that formatBreadcrumb preserves the id from the existing breadcrumb via object spread, and the pages_breadcrumbs table requires unique id values across all rows regardless of locale.
If two locales ever share a breadcrumb id for any reason, publishing becomes impossible.
The integration test forces this state to demonstrate the failure. The suggested fix is for formatBreadcrumb to always generate a fresh id rather than inheriting it from the spread.
- Create a Payload project with
@payloadcms/plugin-nested-docs, PostgreSQL, and at least two locales - Clone the linked repo, run
pnpm install, set up a PostgreSQL database, and configure.env - Run
pnpm test:int— the test creates a page, forces both locales to share a breadcrumb ID, then fails on publish with "The following field is invalid: id" / "Value must be unique"
Which area(s) are affected?
plugin: nested-docs, db: postgres
Environment Info
Payload: 3.79.0
Next.js: 15.5.12
Node: 22.22.0
@payloadcms/plugin-nested-docs: 3.79.0
@payloadcms/db-postgres: 3.79.0
@payloadcms/drizzle: 3.79.0
React: 19.2.4
OS: Linux x64
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with formatBreadcrumb.js and populateBreadcrumbs.js to trace how an existing breadcrumb ID reaches localized rows. Run the linked reproduction's pnpm test:int integration test, then verify that publishing multiple locales creates unique breadcrumb IDs without the PostgreSQL constraint error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- postgresql, typescript
- Domain
- backend, databases
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 74/100