payloadcms / payloadcms/payload

plugin-nested-docs: duplicate breadcrumb IDs across locales cause unique constraint violation on publish

Open Beginner friendly
#16,054 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

db: postgres plugin: nested-docs status: needs-triage
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.

  1. Create a Payload project with @payloadcms/plugin-nested-docs, PostgreSQL, and at least two locales
  2. Clone the linked repo, run pnpm install, set up a PostgreSQL database, and configure .env
  3. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.