payloadcms / payloadcms/payload
Sparse blocks array persisted with gapped _order (db-postgres) — undefined block row bricks the entire edit view
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
A block add through the admin UI persisted a sparse blocks array to Postgres: the surviving rows kept their original array indexes (_order 1 and 3) with no row at _order 2. On every subsequent read, Payload rehydrates the gap as an undefined array entry, and the form-state builder throws:
Block with type "undefined" was found in block data, but no block with that type is defined in the config for field with schema path promotionListings.layout.
The result is that the entire edit view for the document fails to load in the affected locale — editors are locked out of the document until the underlying rows are repaired (a draft save that happens to rewrite the array also clears it).
This is the same end-state reported in #10485 (closed as unreproducible, now locked — the bot's closing message says to open a new issue if it persists). We hit it on payload 3.84.1, i.e. long after the form-state overhaul (#11946) that was believed to make it unreproducible, and we have DB forensics narrowing down when and how the bad data was written.
Forensic evidence
Collection: drafts enabled (localizeStatus: true, schedulePublish: true, maxPerDoc: 50), a localized: true blocks field named layout with 4 block types, @payloadcms/db-postgres.
Version-table trace for the affected document (version_updated_by_id joined, block rows unioned per version, one locale shown):
| Version | Saved (UTC) | Action | layout rows (type:_order) |
|---|---|---|---|
| v4/v5 | 2026-07-22 08:50 | publish (locale A) | header:1 |
| v6/v7 | 2026-07-22 08:52 | publish (locale A) | header:1, cardGrid:3 ← gap born here |
| v8/v9 | 2026-07-23 12:28 | publish (locale B) | header:1, cardGrid:3 (inherited) |
Between two saves 2 minutes apart, a block add went from 1 block to rows at positions 1 and 3 — the middle row was silently dropped at write time while its neighbour kept index 3. This was not a reorder/delete; the same editing session also produced a This field requires at least 1 Rows. validation error on one of the block types (an array field with minRows: 1), which may be the row that vanished. Only one locale of the document was affected; the corruption then propagated into every subsequent version snapshot, and into the published tables on publish.
Two independent problems
- Write path: the postgres adapter accepts a sparse/invalid rows array and persists non-contiguous
_ordervalues instead of rejecting the write or compacting the array. Whatever produces the undefined middle row client-side, the server persisting it turns a UI glitch into durable data corruption. - Read path:
addFieldStatePromisethrows on anundefinedblock row, which fails form-state for the whole document. A degraded render (skip the row / show an error placeholder, as requested in #10485) would keep the document editable so editors could fix it themselves.
We'd be happy to contribute a PR for (2) if maintainers agree with the skip-with-warning approach.
Link to the code that reproduces this issue
https://github.com/infonomic/payload-block-error
Reproduction Steps
Note on the reproduction link: it is the repro repo from #10485 (by @58bits), which reproduces the same failure class. We cannot yet reproduce the client-side trigger on demand — matching #10485 — but the corrupt state and the resulting lock-out reproduce deterministically:
- Create a blank app (
pnpx create-payload-app@latest -t blank) with@payloadcms/db-postgres, drafts enabled, and a blocks field (e.g.layout) with 2+ block types. Localization is how we hit it in production but is not required for the read-path failure. - Create a document with 3 blocks in
layoutand publish/save it. - Simulate the sparse write our production trace shows (a middle row dropped at write time while the neighbour keeps its index) by removing the middle block row directly in the DB, leaving
_order1 and 3 — e.g.DELETE FROM <collection>_blocks_<middle_block> WHERE _parent_id = <doc_id> AND _order = 2;(do the same in the_<collection>_v_blocks_*version tables for the latest draft version). - Open the document in the admin panel.
- Observed: the edit view fails to load; the server logs
Block with type "undefined" was found in block data, but no block with that type is defined in the config for field with schema path <collection>.layouton every form-state request. The document is uneditable. - Expected: the write path never persists a gapped
_ordersequence (reject or compact), and/or the form-state builder degrades gracefully (skip the undefined row with a warning) so the document stays editable.
Which area(s) are affected?
db: postgres, area: ui
Environment Info
Production (where the corruption was written): Payload 3.84.1, Next.js 16.2.9, Node.js 22 (Docker on Kubernetes), Postgres 18 (AWS RDS), multiple locales, localized blocks field, drafts with localizeStatus.
`pnpm payload info` from the affected project:
Relevant Packages:
payload: 3.84.1
next: 16.2.9
@payloadcms/db-postgres: 3.84.1
@payloadcms/live-preview-react: 3.84.1
@payloadcms/next/utilities: 3.84.1
@payloadcms/plugin-form-builder: 3.84.1
@payloadcms/plugin-import-export: 3.84.1
@payloadcms/plugin-mcp: 3.84.1
@payloadcms/plugin-nested-docs: 3.84.1
@payloadcms/plugin-redirects: 3.84.1
@payloadcms/plugin-search: 3.84.1
@payloadcms/plugin-seo: 3.84.1
@payloadcms/richtext-lexical: 3.84.1
@payloadcms/storage-s3: 3.84.1
@payloadcms/ui/shared: 3.84.1
react: 19.2.4
react-dom: 19.2.4
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 the addFieldStatePromise form-state path and the linked reproduction repository, then trace how the Postgres adapter writes block _order values. Reproduce the gapped rows from the SQL steps and inspect the form-state request failure. Done means the write path rejects or compacts gaps, or the form state handles an undefined row without locking the document.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nextjs, postgresql, typescript
- Domain
- databases, frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100