payloadcms / payloadcms/payload
Deleting a user leaves their document locks and preferences behind (Postgres): an ownerless lock crashes the 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
On Postgres, deleting a user (an auth collection document) leaves two kinds of rows behind, and one of them crashes the admin for everyone.
- Document locks owned by the deleted user are never removed, and an ownerless lock blanks the edit view.
payload_locked_documents_rels.users_idisON DELETE cascade, so the delete empties the rels row that names the user but leaves the parentpayload_locked_documentsrow in place with nouser. Opening that document then throws ingetIsLocked: it readsdocs[0].user?.valueand passesundefinedtoextractID, which doesobjectOrID.id→TypeError: Cannot read properties of undefined (reading 'id'). The edit view renders blank for every user until the lock passeslockDocuments.duration(5 minutes by default). We hit it on a Site Settings global after deleting the admin who had it open. - The deleted user's preferences are never deleted. Both delete operations call
deleteUserPreferencesafterdb.deleteOnehas removed the user row. Its query (user.value in ids) goes throughpayload_preferences_rels, whoseusers_idrow the cascade has already removed, so it matches nothing. The preference rows accumulate (a small site of ours had 67 of them).
Neither delete operation removes locks owned by the user: checkDocumentLockStatus only clears locks on the document being deleted.
Link to the code path
On 3.x (head be896e3ee, 14 Sep 2026; unchanged since 1 Aug):
packages/next/src/views/Document/getIsLocked.tslines 105–108 →packages/payload/src/utilities/extractID.tsline 8packages/payload/src/collections/operations/deleteByID.ts:db.deleteOneline 186,deleteUserPreferencesline 205packages/payload/src/collections/operations/delete.ts: lines 219 and 332
On main the same code is at packages/ui/src/utilities/getIsLocked.ts.
Reproduction Steps
Postgres adapter, default lockDocuments.
- Create two admin users, A and B.
- As user B, open any document's edit view (a global works too) and leave it open, so a lock row exists with
user= B. - As user A, delete user B (single delete or the list view's bulk delete).
- As user A, open the document from step 2 within 5 minutes → blank edit view, server log:
TypeError: Cannot read properties of undefined (reading 'id')fromgetIsLocked. - Check the tables:
payload_locked_documentsstill has the row (with nopayload_locked_documents_relsrow for a user), andpayload_preferencesstill has every row B ever wrote.
Confirmed on 3.86.0 (installed) by an integration test that bypasses our workaround hook; the code is the same on 3.89.0 and main.
Expected Behavior
Deleting a user removes the locks they own and their preferences, and the documents they had open stay editable.
Actual Behavior
Locks and preferences outlive the user; an ownerless lock throws in getIsLocked and blanks the document for everyone for up to lockDocuments.duration.
Proposed fix
In both delete operations, for auth collections, delete the user's payload-locked-documents rows (except a lock on their own document, which checkDocumentLockStatus already handles) and their preferences before db.deleteOne, so the rels rows still exist for the queries to match. And make getIsLocked treat a lock with no user as not locked, since existing databases already hold ownerless rows:
// getIsLocked.ts
const lockUser = docs[0]?.user?.value
if (!lockUser) return { isLocked: false, ... }
Environment
- Payload 3.86.0 (behaviour confirmed by test); 3.89.0 and
mainread, same code @payloadcms/db-postgres(Neon), Next.js 16, Node 22
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 packages/payload/src/collections/operations/deleteByID.ts and delete.ts, then trace packages/next/src/views/Document/getIsLocked.ts and extractID.ts; on main, the lock utility is packages/ui/src/utilities/getIsLocked.ts. Run the described Postgres reproduction or integration test, covering both single and bulk deletion. Done means deleting an auth user removes their owned locks and preferences, existing ownerless locks do not crash the edit view, and the affected documents remain editable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nextjs, postgresql, typescript
- Domain
- authentication, backend, databases, frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100