payloadcms / payloadcms/payload

Deleting a user leaves their document locks and preferences behind (Postgres): an ownerless lock crashes the edit view

Open
#18,200 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Bug db: postgres
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.

  1. Document locks owned by the deleted user are never removed, and an ownerless lock blanks the edit view. payload_locked_documents_rels.users_id is ON DELETE cascade, so the delete empties the rels row that names the user but leaves the parent payload_locked_documents row in place with no user. Opening that document then throws in getIsLocked: it reads docs[0].user?.value and passes undefined to extractID, which does objectOrID.idTypeError: Cannot read properties of undefined (reading 'id'). The edit view renders blank for every user until the lock passes lockDocuments.duration (5 minutes by default). We hit it on a Site Settings global after deleting the admin who had it open.
  2. The deleted user's preferences are never deleted. Both delete operations call deleteUserPreferences after db.deleteOne has removed the user row. Its query (user.value in ids) goes through payload_preferences_rels, whose users_id row 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.ts lines 105–108 → packages/payload/src/utilities/extractID.ts line 8
  • packages/payload/src/collections/operations/deleteByID.ts: db.deleteOne line 186, deleteUserPreferences line 205
  • packages/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.

  1. Create two admin users, A and B.
  2. 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.
  3. As user A, delete user B (single delete or the list view's bulk delete).
  4. 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') from getIsLocked.
  5. Check the tables: payload_locked_documents still has the row (with no payload_locked_documents_rels row for a user), and payload_preferences still 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 main read, same code
  • @payloadcms/db-postgres (Neon), Next.js 16, Node 22

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 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.